dsh-plugin-prompt-tool 0.4.2 → 0.5.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 +125 -222
- package/engine/compaction-epoch.mjs +139 -0
- package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
- package/engine/compositions/library/compaction.yml +52 -0
- package/engine/compositions/library/context-gate.yml +40 -0
- package/engine/compositions/library/custom-bash.yml +14 -0
- package/engine/compositions/library/delegation.yml +85 -0
- package/engine/compositions/library/official-agent-instructions.yml +17 -0
- package/engine/compositions/library/official-persistent-shell.yml +56 -0
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/official-tool-bash.yml +6 -0
- package/engine/compositions/library/official-tool-cordis.yml +13 -0
- package/engine/compositions/library/official-tool-presentation.yml +7 -0
- package/engine/compositions/library/official-tool-skill.yml +14 -0
- package/engine/compositions/library/persistent-shell.yml +36 -0
- package/engine/compositions/library/persona.yml +9 -0
- package/engine/compositions/library/planning.yml +41 -0
- package/engine/compositions/library/prompt-config-engine.yml +16 -0
- package/engine/compositions/library/router-first-turn.yml +9 -0
- package/engine/compositions/library/run-code-env.yml +15 -0
- package/engine/compositions/library/skill-filesystem.yml +13 -0
- package/engine/compositions/library/skill-search.yml +14 -0
- package/engine/compositions/library/str-replace-editor.yml +10 -0
- package/engine/compositions/library/tool-ask-user.yml +8 -0
- package/engine/compositions/library/tool-bash.yml +16 -0
- package/engine/compositions/library/tool-bootstrap.yml +16 -0
- package/engine/compositions/library/tool-filter.yml +12 -0
- package/engine/compositions/library/tool-fs-search.yml +18 -0
- package/engine/compositions/library/tool-fs.yml +10 -0
- package/engine/compositions/library/tool-goal.yml +19 -0
- package/engine/compositions/library/tool-jobs.yml +23 -0
- package/engine/compositions/library/tool-pwsh.yml +12 -0
- package/engine/compositions/library/tool-todo.yml +11 -0
- package/engine/compositions/library/tool-web.yml +9 -0
- package/engine/compositions/source/local/context-gate.yml +37 -0
- package/engine/compositions/source/local/custom-bash.yml +11 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
- package/engine/compositions/source/local/router-first-turn.yml +5 -0
- package/engine/compositions/source/local/run-code-env.yml +12 -0
- package/engine/compositions/source/local/skill-search.yml +11 -0
- package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
- package/engine/context-gate.mjs +305 -0
- package/{preset → engine}/custom-bash.mjs +243 -243
- package/engine/executor.mjs +271 -0
- package/engine/fillers.mjs +273 -0
- package/engine/layers.mjs +220 -0
- package/engine/prompt-config-engine.mjs +49 -0
- package/{preset → engine}/router-first-turn.mjs +85 -73
- package/engine/run-code-env.mjs +208 -0
- package/engine/schema.mjs +294 -0
- package/{preset → engine}/shared.mjs +54 -0
- package/engine/strategies.mjs +223 -0
- package/{preset → engine}/tool-bootstrap.mjs +350 -282
- package/engine/tool-filter.mjs +80 -0
- package/engine/vendor/yaml/LICENSE +13 -0
- package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
- package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
- package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
- package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
- package/engine/vendor/yaml/dist/compose/composer.js +219 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
- package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
- package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
- package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
- package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
- package/engine/vendor/yaml/dist/doc/Document.js +335 -0
- package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
- package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
- package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
- package/engine/vendor/yaml/dist/doc/directives.js +176 -0
- package/engine/vendor/yaml/dist/errors.js +57 -0
- package/engine/vendor/yaml/dist/index.js +17 -0
- package/engine/vendor/yaml/dist/log.js +11 -0
- package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
- package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
- package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
- package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
- package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
- package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
- package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
- package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
- package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
- package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
- package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
- package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
- package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
- package/engine/vendor/yaml/dist/parse/cst.js +98 -0
- package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
- package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
- package/engine/vendor/yaml/dist/parse/parser.js +975 -0
- package/engine/vendor/yaml/dist/public-api.js +102 -0
- package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
- package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
- package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
- package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
- package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
- package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
- package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
- package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
- package/engine/vendor/yaml/dist/schema/tags.js +96 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
- package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
- package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
- package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
- package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
- package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
- package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
- package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
- package/engine/vendor/yaml/dist/util.js +11 -0
- package/engine/vendor/yaml/dist/visit.js +233 -0
- package/engine/vendor/yaml/index.js +5 -0
- package/engine/vendor/yaml/package.json +11 -0
- package/lib/client.js +4421 -781
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +504 -51
- package/lib/index.mjs +3871 -731
- package/lib/preset-core.d.mts +7 -37
- package/lib/preset-core.mjs +44 -283
- package/lib/prompt-configs-BcP6miu9.d.mts +102 -0
- package/lib/prompt-configs-k7wuZs0C.mjs +719 -0
- package/package.json +104 -98
- package/preset/anchored/preset.yml +344 -0
- package/preset/creative/preset.yml +62 -0
- package/preset/custom/preset.yml +13 -0
- package/preset/liangshen/preset.yml +66 -0
- package/preset/minimal/preset.yml +41 -0
- package/preset/ptc/preset.yml +50 -0
- package/preset/standard/preset.yml +49 -0
- package/skills/manifest.json +7 -0
- package/skills/sandboxmod/SKILL.md +49 -49
- package/skills/web ui/SKILL.md +42 -0
- package/templates/10-pre-step.yml +28 -0
- package/templates/11-merged-a.yml +11 -0
- package/templates/13-anchor.yml +19 -0
- package/templates/14-first-turn-anchor.yml +27 -0
- package/templates/15-guide-auto.yml +25 -0
- package/templates/16-custom-fallback.yml +21 -0
- package/templates/17-instruction-hint.yml +23 -0
- package/templates/18-placeholder-env-facts.yml +11 -0
- package/templates/19-placeholder-skill-catalog.yml +19 -0
- package/templates/20-system-section.yml +14 -0
- package/templates/30-runtime-context.yml +10 -0
- package/templates/31-runtime-context-placeholder.yml +18 -0
- package/templates/40-agent-request.yml +11 -0
- package/templates/50-llm-stream.yml +9 -0
- package/templates/60-tool-pipeline.yml +12 -0
- package/AGENTS.md +0 -4
- package/plan.md +0 -312
- package/preset/agent.cordis.yml +0 -443
- package/preset/compaction-epoch.mjs +0 -81
- package/preset/context-gate.mjs +0 -165
- package/preset/instruction-hint.mjs +0 -217
- package/preset/near-anchor.mjs +0 -101
- package/preset/preset.yml +0 -3
- package/preset/prompt-injector.mjs +0 -112
- package/preset/router-guide.mjs +0 -79
- package/preset.md +0 -115
- package/upstream/dsh-anchored-standard/LICENSE +0 -22
- package/upstream/dsh-anchored-standard/NOTICE +0 -19
- package/upstream/dsh-anchored-standard/REVISION +0 -1
- package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
- package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
- package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
- package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
- package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
- package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
- package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
- package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
- /package/{preset → engine}/skill-search.mjs +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* executor — pre-step 消息批执行器。
|
|
3
|
+
* 职责:过滤(层/子代理/模型/晋升)→ resolve → 插值 → 去重/合并 → 落位插入。
|
|
4
|
+
* 任一提示词配置失败只跳过该提示词配置并 warnOnce,绝不让注入 bug 卡死会话。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
PROMOTE_EVENTS,
|
|
9
|
+
createWarnOnce,
|
|
10
|
+
interpolateVariables,
|
|
11
|
+
isDelegated,
|
|
12
|
+
matchesModel,
|
|
13
|
+
newMessageId,
|
|
14
|
+
} from './shared.mjs'
|
|
15
|
+
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
16
|
+
import { wireLayers } from './layers.mjs'
|
|
17
|
+
|
|
18
|
+
const name = 'prompt-config-engine'
|
|
19
|
+
|
|
20
|
+
/** 事件数据兼容两层形状:event.data 本身是消息,或 event.data.message 是消息。 */
|
|
21
|
+
function eventMessage(event) {
|
|
22
|
+
const data = event?.data
|
|
23
|
+
if (data === null || typeof data !== 'object') return undefined
|
|
24
|
+
const message = data.message !== null && typeof data.message === 'object' ? data.message : data
|
|
25
|
+
return message?.source !== null && typeof message?.source === 'object' ? message : undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 合并身份:merged 模式按位置分组(merged:<position>);separate 模式保持自身身份。 */
|
|
29
|
+
function mergedIdentity(config) {
|
|
30
|
+
if (config.mergeMode !== 'merged') return config.id
|
|
31
|
+
return `merged:${config.position}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasInjected(config, session) {
|
|
35
|
+
const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
|
|
36
|
+
return (Array.isArray(session.events) ? session.events : []).some((event) => {
|
|
37
|
+
const message = eventMessage(event)
|
|
38
|
+
// 双通道去重:kind(外来/第三方消息,如 context-gate 的 instruction-hint)或
|
|
39
|
+
// plugin(本引擎注入的命名空间,merged 组用 merged:<position>)。
|
|
40
|
+
return message?.source?.kind === config.sourceKind || message?.source?.plugin === value
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 当前消息批内是否已有该提示词配置注入(每轮去重)。 */
|
|
45
|
+
function hasInBatch(config, messages) {
|
|
46
|
+
const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
|
|
47
|
+
return messages.some((message) =>
|
|
48
|
+
message?.source?.kind === config.sourceKind || message?.source?.plugin === value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 构造默认 user/assistant 消息;策略返回完整 patch 时覆盖对应字段。 */
|
|
52
|
+
function buildMessage(config, resolved) {
|
|
53
|
+
const text = typeof resolved.text === 'string' ? resolved.text : ''
|
|
54
|
+
const defaultContent = config.texts.length > 0
|
|
55
|
+
? config.texts.map((item) => ({ type: 'text', text: item }))
|
|
56
|
+
: [{ type: 'text', text }]
|
|
57
|
+
const sourceValue = mergedIdentity(config)
|
|
58
|
+
return {
|
|
59
|
+
id: typeof resolved.id === 'string' && resolved.id.length > 0 ? resolved.id : newMessageId(config.id),
|
|
60
|
+
role: typeof resolved.role === 'string' ? resolved.role : config.role,
|
|
61
|
+
content: Array.isArray(resolved.content) ? resolved.content : defaultContent,
|
|
62
|
+
source: resolved.source !== null && typeof resolved.source === 'object'
|
|
63
|
+
? resolved.source
|
|
64
|
+
: {
|
|
65
|
+
kind: config.sourceKind,
|
|
66
|
+
plugin: sourceValue,
|
|
67
|
+
...(typeof config.form === 'string' ? { form: config.form } : {}),
|
|
68
|
+
...(typeof config.summary === 'string' && config.summary.length > 0 ? { summary: config.summary } : {}),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 按提示词配置声明的插入位置写入消息批;找不到插入锚点返回 false(本提示词配置跳过)。 */
|
|
74
|
+
function insertMessage(config, messages, message) {
|
|
75
|
+
if (config.position === 'before-all') {
|
|
76
|
+
messages.unshift(message)
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
if (config.position === 'after-all') {
|
|
80
|
+
messages.push(message)
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
// 默认 after-user:只锚真实用户消息,插件消息不算。
|
|
84
|
+
const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
|
|
85
|
+
if (userIndex < 0) return false
|
|
86
|
+
messages.splice(userIndex + 1, 0, message)
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 把一组运行时提示词配置装配为注入执行器。
|
|
92
|
+
* @param prepend 是否以 prepend 注册 pre-step(合并行恒 true;单条提示词配置兼容层由参数决定)。
|
|
93
|
+
*/
|
|
94
|
+
export function applyPromptConfigs(ctx, configs, options = {}) {
|
|
95
|
+
const list = configs.filter((config) => config !== undefined && config !== null)
|
|
96
|
+
if (list.length === 0) return
|
|
97
|
+
// 互斥组:同一 group 且 exclusive=true 时,只保留排序后第一个 enabled 提示词配置。
|
|
98
|
+
const claimedGroups = new Set()
|
|
99
|
+
const effectiveList = list.filter((config) => {
|
|
100
|
+
if (config.enabled === false) return false
|
|
101
|
+
if (config.group !== undefined && config.exclusive === true) {
|
|
102
|
+
if (claimedGroups.has(config.group)) return false
|
|
103
|
+
claimedGroups.add(config.group)
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
})
|
|
107
|
+
const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
|
|
108
|
+
const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true })
|
|
109
|
+
ctx.on('session/event', (session, event) => {
|
|
110
|
+
main.observe(session, event)
|
|
111
|
+
withSubagents.observe(session, event)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
/** 每提示词配置每会话的进程内快路径;真相在持久事件流。 */
|
|
115
|
+
// ponytail: 会话数上限防无界增长(Set 按 session.id 累积);若需精确淘汰改 LRU。
|
|
116
|
+
const MAX_MEMO_SESSIONS = 4096
|
|
117
|
+
const injectedMemo = new Map()
|
|
118
|
+
const configMemo = (config) => {
|
|
119
|
+
let memo = injectedMemo.get(config.id)
|
|
120
|
+
if (memo === undefined) {
|
|
121
|
+
if (injectedMemo.size >= MAX_MEMO_SESSIONS) injectedMemo.clear()
|
|
122
|
+
memo = new Set()
|
|
123
|
+
injectedMemo.set(config.id, memo)
|
|
124
|
+
}
|
|
125
|
+
return memo
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const warnOnce = createWarnOnce(ctx, name)
|
|
129
|
+
const prepend = options.prepend === true || list.some((config) => config.prepend === true)
|
|
130
|
+
|
|
131
|
+
// 非 pre-step 提示词配置接入各自声明的官方层级通道(system-section /
|
|
132
|
+
// runtime-context / agent-request / llm-stream / tool-pipeline)。
|
|
133
|
+
wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
|
|
134
|
+
|
|
135
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
136
|
+
const decision = await next()
|
|
137
|
+
try {
|
|
138
|
+
if (decision.kind === 'reject') return decision
|
|
139
|
+
if (agent === undefined) return decision
|
|
140
|
+
const session = agent.session
|
|
141
|
+
if (session === undefined) return decision
|
|
142
|
+
|
|
143
|
+
const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
|
|
144
|
+
let changed = false
|
|
145
|
+
|
|
146
|
+
const due = []
|
|
147
|
+
for (const config of effectiveList) {
|
|
148
|
+
try {
|
|
149
|
+
if (config.layer !== 'pre-step') continue
|
|
150
|
+
const delegated = isDelegated(session)
|
|
151
|
+
if (config.audience === 'main' && delegated) continue
|
|
152
|
+
if (config.audience === 'subagent' && !delegated) continue
|
|
153
|
+
if (!matchesModel(config.modelScope, agent.options?.model)) continue
|
|
154
|
+
if (config.promotion === 'main' && !main.status(agent).promoted) continue
|
|
155
|
+
if (config.promotion === 'include-subagents' && !withSubagents.status(agent).promoted) continue
|
|
156
|
+
|
|
157
|
+
const memo = configMemo(config)
|
|
158
|
+
if (config.dedupe === 'session') {
|
|
159
|
+
if (memo.has(session.id) || hasInjected(config, session)) {
|
|
160
|
+
memo.add(session.id)
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
} else if (config.dedupe === 'batch' && hasInBatch(config, messages)) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const resolved = await config.resolve({ ctx, agent, session, decision, messages })
|
|
168
|
+
if (resolved === null || resolved === undefined) continue
|
|
169
|
+
const patched = { ...resolved }
|
|
170
|
+
// params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
|
|
171
|
+
//(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
|
|
172
|
+
const mergedVars = { ...config.variables, ...config.params, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
173
|
+
if (typeof patched.text === 'string') {
|
|
174
|
+
// 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
|
|
175
|
+
patched.text = interpolateVariables(patched.text, mergedVars, session)
|
|
176
|
+
}
|
|
177
|
+
if (config.texts.length > 0) {
|
|
178
|
+
const blocks = config.texts
|
|
179
|
+
.map((item) => interpolateVariables(item, mergedVars, session))
|
|
180
|
+
.filter((item) => item.length > 0)
|
|
181
|
+
.map((item) => ({ type: 'text', text: item }))
|
|
182
|
+
if (blocks.length > 0) patched.content = blocks
|
|
183
|
+
}
|
|
184
|
+
const hasText = typeof patched.text === 'string' && patched.text.length > 0
|
|
185
|
+
const hasContent = Array.isArray(patched.content) && patched.content.length > 0
|
|
186
|
+
if (!hasText && !hasContent) continue
|
|
187
|
+
due.push({ config, resolved: patched })
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// 单个提示词配置失败不得伤及会话:跳过该提示词配置,只告警一次。
|
|
190
|
+
warnOnce(`${name}: config ${String(config?.id ?? '<unknown>')} failed, skipping: ${String((error && error.message) || error)}`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// order 升序(同值保持声明顺序):决定拼接顺序与同位置插入顺序。
|
|
195
|
+
due.sort((a, b) => a.config.order - b.config.order)
|
|
196
|
+
|
|
197
|
+
// merged 模式的多条提示词配置在首条配置的位置合并为一条消息;
|
|
198
|
+
// 文本按内容块拼接,source 身份改用 merged:<position> 以保持持久幂等。
|
|
199
|
+
const orderedGroups = []
|
|
200
|
+
const groupIndex = new Map()
|
|
201
|
+
for (const entry of due) {
|
|
202
|
+
const group = entry.config.mergeMode === 'merged'
|
|
203
|
+
? mergedIdentity(entry.config)
|
|
204
|
+
: undefined
|
|
205
|
+
if (group === undefined) {
|
|
206
|
+
orderedGroups.push([entry])
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
let index = groupIndex.get(group)
|
|
210
|
+
if (index === undefined) {
|
|
211
|
+
index = orderedGroups.length
|
|
212
|
+
groupIndex.set(group, index)
|
|
213
|
+
orderedGroups.push([])
|
|
214
|
+
}
|
|
215
|
+
orderedGroups[index].push(entry)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const planned = []
|
|
219
|
+
for (const group of orderedGroups) {
|
|
220
|
+
const base = group[0]
|
|
221
|
+
const message = buildMessage(base.config, base.resolved)
|
|
222
|
+
if (group.length > 1) {
|
|
223
|
+
message.content = group.flatMap((entry) => {
|
|
224
|
+
if (Array.isArray(entry.resolved.content)) return entry.resolved.content
|
|
225
|
+
return typeof entry.resolved.text === 'string' && entry.resolved.text.length > 0
|
|
226
|
+
? [{ type: 'text', text: entry.resolved.text }]
|
|
227
|
+
: []
|
|
228
|
+
})
|
|
229
|
+
if (message.source !== null && typeof message.source === 'object') {
|
|
230
|
+
message.source = { ...message.source, plugin: mergedIdentity(base.config) }
|
|
231
|
+
}
|
|
232
|
+
if (new Set(group.map((entry) => entry.config.position)).size > 1) {
|
|
233
|
+
warnOnce(`${name}: merged group mixes positions — using ${String(base.config.position)} from the first config`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
planned.push({ position: base.config.position, message, group })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 同位置批量插入:planned 已按 order 升序,多元素 splice/unshift/push 保持该顺序。
|
|
240
|
+
const markGroup = (group) => {
|
|
241
|
+
for (const entry of group) {
|
|
242
|
+
if (entry.config.dedupe === 'session') configMemo(entry.config).add(session.id)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const beforeAll = planned.filter((item) => item.position === 'before-all')
|
|
246
|
+
const afterUser = planned.filter((item) => item.position !== 'before-all' && item.position !== 'after-all')
|
|
247
|
+
const afterAll = planned.filter((item) => item.position === 'after-all')
|
|
248
|
+
if (beforeAll.length > 0) {
|
|
249
|
+
messages.unshift(...beforeAll.map((item) => item.message))
|
|
250
|
+
for (const item of beforeAll) markGroup(item.group)
|
|
251
|
+
changed = true
|
|
252
|
+
}
|
|
253
|
+
const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
|
|
254
|
+
if (afterUser.length > 0 && userIndex >= 0) {
|
|
255
|
+
messages.splice(userIndex + 1, 0, ...afterUser.map((item) => item.message))
|
|
256
|
+
for (const item of afterUser) markGroup(item.group)
|
|
257
|
+
changed = true
|
|
258
|
+
}
|
|
259
|
+
if (afterAll.length > 0) {
|
|
260
|
+
messages.push(...afterAll.map((item) => item.message))
|
|
261
|
+
for (const item of afterAll) markGroup(item.group)
|
|
262
|
+
changed = true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return changed ? { ...decision, messages } : decision
|
|
266
|
+
} catch (error) {
|
|
267
|
+
warnOnce(`${name}: prompt config failed, skipping: ${String((error && error.message) || error)}`)
|
|
268
|
+
return decision
|
|
269
|
+
}
|
|
270
|
+
}, { prepend })
|
|
271
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fillers — 引擎内置的动态填充器(strategy=placeholder 的数据源)。
|
|
3
|
+
* 全部遵循同一铁律:服务缺失或列表失败时返回 null(跳过该配置),绝不伤及会话。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { getService, parseToolNames } from './shared.mjs'
|
|
8
|
+
|
|
9
|
+
const name = 'prompt-config-engine'
|
|
10
|
+
|
|
11
|
+
const PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
|
|
12
|
+
const USER_GLOBAL_CANDIDATE = 'AGENTS.md'
|
|
13
|
+
|
|
14
|
+
/** instruction-hint 提示文本:agents-instruction.md 优先,否则动态探测。
|
|
15
|
+
* path 可经提示词配置 params.agentsInstructionPath 指定(共享引擎时引擎文件在
|
|
16
|
+
* 预设根 .engine,必须显式指向预设目录内的文件,如 ../<预设>/agents-instruction.md)。 */
|
|
17
|
+
function readAgentsInstructionText(path = './agents-instruction.md') {
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(new URL(path, import.meta.url), 'utf8').trim()
|
|
20
|
+
} catch {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Find the project root: first ancestor containing any root marker (e.g. .git). */
|
|
26
|
+
async function findProjectRoot(fs, cwd, signal) {
|
|
27
|
+
let current = cwd
|
|
28
|
+
for (;;) {
|
|
29
|
+
for (const marker of ['.git', '.hg', '.svn']) {
|
|
30
|
+
try {
|
|
31
|
+
const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
|
|
32
|
+
const info = await fs.stat(target, signal)
|
|
33
|
+
if (info !== undefined) return current
|
|
34
|
+
} catch {
|
|
35
|
+
// Probe failure = marker absent; continue.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const parent = parentPath(current)
|
|
39
|
+
if (parent === current || parent.length === 0) return cwd
|
|
40
|
+
current = parent
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** List instruction files present in one directory (project candidates). */
|
|
45
|
+
async function presentInDir(fs, dir, candidates, signal) {
|
|
46
|
+
const found = []
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
try {
|
|
49
|
+
const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
|
|
50
|
+
const info = await fs.stat(target, signal)
|
|
51
|
+
if (info !== undefined && info.type === 'file') found.push(candidate)
|
|
52
|
+
} catch {
|
|
53
|
+
// Absent or unreadable — skip.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return found
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Join one path segment onto a directory (platform-agnostic string join). */
|
|
60
|
+
function joinPath(dir, segment) {
|
|
61
|
+
if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
|
|
62
|
+
const sep = dir.includes('\\') ? '\\' : '/'
|
|
63
|
+
return dir + sep + segment
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parent of an absolute Windows or POSIX path. */
|
|
67
|
+
function parentPath(path) {
|
|
68
|
+
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
69
|
+
if (idx <= 0) return path
|
|
70
|
+
const parent = path.slice(0, idx)
|
|
71
|
+
return parent.length === 0 ? path : parent
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* instruction-hint:晋升后一次性提示指令文件存在(agents-instruction.md 优先)。
|
|
76
|
+
* 可自定义参数:params.text 直接作为提示文本(覆盖文件与动态探测);
|
|
77
|
+
* 其余字段语义与 env-facts / skill-catalog 一致(config.params 由单一配置源下发)。
|
|
78
|
+
*/
|
|
79
|
+
function createInstructionHintResolver(config) {
|
|
80
|
+
const customText = typeof config?.params?.text === 'string' && config.params.text.trim().length > 0
|
|
81
|
+
? config.params.text.trim()
|
|
82
|
+
: ''
|
|
83
|
+
// 共享引擎:引擎文件在预设根 .engine,agents-instruction.md 必须经
|
|
84
|
+
// params.agentsInstructionPath 显式指向预设目录内文件(旧版无此参数时回退
|
|
85
|
+
// './agents-instruction.md'——引擎文件与内容资产同目录的旧布局;显式 .txt 旧路径
|
|
86
|
+
// 仍可读(升级前生成的组合,文件未重建前保持兼容))。
|
|
87
|
+
const instructionPath = typeof config?.params?.agentsInstructionPath === 'string'
|
|
88
|
+
&& config.params.agentsInstructionPath.trim().length > 0
|
|
89
|
+
? config.params.agentsInstructionPath
|
|
90
|
+
: './agents-instruction.md'
|
|
91
|
+
const agentsInstructionText = readAgentsInstructionText(instructionPath)
|
|
92
|
+
return async ({ ctx, agent, session }) => {
|
|
93
|
+
const id = `instruction-hint-${session.id}`
|
|
94
|
+
if (customText.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
text: customText,
|
|
98
|
+
source: { kind: 'instruction-hint', form: 'hint' },
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (agentsInstructionText.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
text: agentsInstructionText,
|
|
105
|
+
source: { kind: 'instruction-hint', form: 'hint' },
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const fs = ctx.get('fs')
|
|
109
|
+
if (fs === undefined) return null
|
|
110
|
+
const cwd = session.header?.cwd ?? process.cwd()
|
|
111
|
+
const projectFiles = []
|
|
112
|
+
const root = await findProjectRoot(fs, cwd, agent.signal)
|
|
113
|
+
projectFiles.push(...await presentInDir(fs, root, PROJECT_CANDIDATES, agent.signal))
|
|
114
|
+
const userGlobalFiles = []
|
|
115
|
+
try {
|
|
116
|
+
const dshHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
|
|
117
|
+
if (dshHome !== undefined) {
|
|
118
|
+
userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_CANDIDATE], agent.signal))
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Unreadable home probe — ignore.
|
|
122
|
+
}
|
|
123
|
+
const sections = []
|
|
124
|
+
if (projectFiles.length > 0) {
|
|
125
|
+
sections.push(`Workspace instruction files exist: ${projectFiles.join(', ')} (project root: ${root}).`)
|
|
126
|
+
}
|
|
127
|
+
if (userGlobalFiles.length > 0) {
|
|
128
|
+
sections.push(`A user-global instruction file exists: ${USER_GLOBAL_CANDIDATE}.`)
|
|
129
|
+
}
|
|
130
|
+
if (sections.length === 0) return null
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
text: [
|
|
134
|
+
...sections,
|
|
135
|
+
'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
|
|
136
|
+
].join(' '),
|
|
137
|
+
source: { kind: 'instruction-hint', form: 'hint' },
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* env-facts:机器事实动态填充器。
|
|
144
|
+
* params.envKeys 逗号分隔环境变量白名单,默认 DSH_HOME,DSH_WORKSPACE;
|
|
145
|
+
* CWD 特殊映射到 session.header.cwd ?? process.cwd()。
|
|
146
|
+
* 返回 facts 变量表与默认文本;用户可用 text 模板 + {{变量}} 完全自定义输出。
|
|
147
|
+
*/
|
|
148
|
+
function createEnvFactsResolver(config) {
|
|
149
|
+
const keys = parseToolNames(typeof config.params?.envKeys === 'string' && config.params.envKeys.length > 0
|
|
150
|
+
? config.params.envKeys
|
|
151
|
+
: 'DSH_HOME,DSH_WORKSPACE')
|
|
152
|
+
return ({ agent }) => {
|
|
153
|
+
const session = agent?.session
|
|
154
|
+
const cwd = session?.header?.cwd ?? process.cwd()
|
|
155
|
+
const builtinHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : '')
|
|
156
|
+
const facts = {}
|
|
157
|
+
for (const key of keys) {
|
|
158
|
+
let value
|
|
159
|
+
if (key === 'CWD' || key === 'WORKSPACE') value = cwd
|
|
160
|
+
else if (key === 'DSH_HOME') value = builtinHome
|
|
161
|
+
else value = process.env[key]
|
|
162
|
+
if (typeof value === 'string' && value.length > 0) facts[key] = value
|
|
163
|
+
}
|
|
164
|
+
facts.WORKSPACE = process.env.DSH_WORKSPACE ?? cwd
|
|
165
|
+
facts.CWD = cwd
|
|
166
|
+
const defaultText = ['Environment facts:', ...Object.entries(facts).map(([key, value]) => `- ${key}=${value}`)].join('\n')
|
|
167
|
+
return { text: defaultText, variables: facts }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* skill-catalog:技能目录动态填充器。
|
|
173
|
+
* 数据来自宿主 skills 服务(聚合各 provider;本插件 provider 的 list 已按
|
|
174
|
+
* skillSwitches 过滤,因此目录天然只含已启用技能)。服务缺失或列表失败时
|
|
175
|
+
* 跳过本配置并告警一次,绝不伤及会话。
|
|
176
|
+
*/
|
|
177
|
+
function createSkillCatalogResolver(config) {
|
|
178
|
+
const limit = Number.isSafeInteger(config.params?.limit) && config.params.limit >= 0
|
|
179
|
+
? config.params.limit
|
|
180
|
+
: 20
|
|
181
|
+
const fields = parseToolNames(typeof config.params?.fields === 'string' ? config.params.fields : 'name,description')
|
|
182
|
+
.filter((field) => ['name', 'description', 'whenToUse'].includes(field))
|
|
183
|
+
const providers = parseToolNames(config.params?.providers)
|
|
184
|
+
const emptyBehavior = config.params?.emptyBehavior === 'text' ? 'text' : 'skip'
|
|
185
|
+
const emptyText = typeof config.params?.emptyText === 'string' && config.params.emptyText.length > 0
|
|
186
|
+
? config.params.emptyText
|
|
187
|
+
: '当前没有可用技能。'
|
|
188
|
+
let warned = false
|
|
189
|
+
const warnOnceLocal = (ctx, message) => {
|
|
190
|
+
if (warned) return
|
|
191
|
+
warned = true
|
|
192
|
+
try {
|
|
193
|
+
ctx?.logger?.warn(message)
|
|
194
|
+
} catch {
|
|
195
|
+
// Logger unavailable — the guard exists only to avoid spamming.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return async ({ ctx, agent, session }) => {
|
|
200
|
+
try {
|
|
201
|
+
const skills = getService(ctx, 'skills')
|
|
202
|
+
if (skills === undefined || typeof skills.list !== 'function') {
|
|
203
|
+
warnOnceLocal(ctx, `${name}: skills service unavailable — skill-catalog config ${config.id} skipped`)
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
const all = await skills.list({
|
|
207
|
+
scope: agent ?? ctx,
|
|
208
|
+
...(session?.header?.cwd !== undefined ? { cwd: session.header.cwd } : {}),
|
|
209
|
+
...(agent?.signal !== undefined ? { signal: agent.signal } : {}),
|
|
210
|
+
})
|
|
211
|
+
const visible = Array.isArray(all) ? all : []
|
|
212
|
+
const scoped = providers.length === 0
|
|
213
|
+
? visible
|
|
214
|
+
: visible.filter((skill) => providers.includes(skill?.provider))
|
|
215
|
+
const total = scoped.length
|
|
216
|
+
if (total === 0) {
|
|
217
|
+
if (emptyBehavior !== 'text') return null
|
|
218
|
+
return { text: emptyText, variables: { SKILL_COUNT: '0', SKILL_NAMES: '', SKILLS_TEXT: '' } }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const nameOf = (skill) => {
|
|
222
|
+
const value = skill?.name
|
|
223
|
+
if (typeof value === 'string' && value.length > 0) return value
|
|
224
|
+
if (typeof skill?.locator === 'string' && skill.locator.length > 0) return skill.locator
|
|
225
|
+
return String(skill?.id ?? '(unnamed)')
|
|
226
|
+
}
|
|
227
|
+
const firstLine = (value) => typeof value === 'string' ? value.split('\n')[0].trim() : ''
|
|
228
|
+
const rows = (limit === 0 ? scoped : scoped.slice(0, limit)).map((skill) => {
|
|
229
|
+
const parts = []
|
|
230
|
+
if (fields.includes('name')) parts.push(nameOf(skill))
|
|
231
|
+
if (fields.includes('description')) {
|
|
232
|
+
const description = firstLine(skill?.description)
|
|
233
|
+
if (description.length > 0) parts.push(description)
|
|
234
|
+
}
|
|
235
|
+
if (fields.includes('whenToUse')) {
|
|
236
|
+
const whenToUse = firstLine(skill?.whenToUse)
|
|
237
|
+
if (whenToUse.length > 0) parts.push(`适用:${whenToUse}`)
|
|
238
|
+
}
|
|
239
|
+
return parts.length === 0 ? '' : `- ${parts.join(': ')}`
|
|
240
|
+
}).filter((line) => line.length > 0)
|
|
241
|
+
const skillsText = rows.join('\n')
|
|
242
|
+
const skillsNames = scoped.map((skill) => nameOf(skill)).join(', ')
|
|
243
|
+
return {
|
|
244
|
+
text: `Available skills (${total}):\n${skillsText}`,
|
|
245
|
+
variables: {
|
|
246
|
+
SKILL_COUNT: String(total),
|
|
247
|
+
SKILL_NAMES: skillsNames,
|
|
248
|
+
SKILLS_TEXT: skillsText,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
warnOnceLocal(ctx, `${name}: skill-catalog config ${config.id} failed, skipping: ${String((error && error.message) || error)}`)
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 动态填充器:strategy=placeholder 时按 fill 键在注入点填充内容。 */
|
|
259
|
+
export const FILLERS = {
|
|
260
|
+
'instruction-hint': (config) => createInstructionHintResolver(config),
|
|
261
|
+
'env-facts': (config) => createEnvFactsResolver(config),
|
|
262
|
+
'skill-catalog': (config) => createSkillCatalogResolver(config),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createPlaceholderResolver(config) {
|
|
266
|
+
const make = FILLERS[config.fill]
|
|
267
|
+
if (make === undefined) {
|
|
268
|
+
throw new TypeError(`${name}: unknown config fill ${JSON.stringify(config.fill)}`)
|
|
269
|
+
}
|
|
270
|
+
return make(config)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { createInstructionHintResolver }
|