dsh-plugin-prompt-tool 1.0.0 → 2.0.1
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 +38 -29
- package/engine/actions/append-context.mjs +49 -0
- package/engine/actions/assembly.mjs +150 -0
- package/engine/actions/catalog.mjs +92 -0
- package/engine/actions/content.mjs +205 -0
- package/engine/actions/decision.mjs +54 -0
- package/engine/actions/examples.mjs +11 -0
- package/engine/actions/guard.mjs +80 -0
- package/engine/actions/inbox-prepend.mjs +37 -0
- package/engine/actions/index.mjs +100 -0
- package/engine/actions/inject-text.mjs +22 -0
- package/engine/actions/pre-step-filter.mjs +55 -0
- package/engine/actions/registration.mjs +168 -0
- package/engine/actions/request-params.mjs +32 -0
- package/engine/actions/sdk-strip.mjs +30 -0
- package/engine/actions/shared.mjs +71 -0
- package/engine/actions.mjs +2 -931
- package/engine/branch.mjs +44 -0
- package/engine/compaction-epoch.mjs +31 -13
- package/engine/compositions/source/local/dev-tool-search.yml +11 -0
- package/engine/compositions/source/local/instruction-hint.yml +22 -1
- package/engine/compositions/source/local/rule-engine.yml +6 -0
- package/engine/condition.mjs +16 -3
- package/engine/conditions/anchor.mjs +32 -0
- package/engine/conditions/availability.mjs +47 -0
- package/engine/conditions/composite.mjs +83 -0
- package/engine/conditions/count.mjs +140 -0
- package/engine/conditions/evaluation.mjs +41 -0
- package/engine/conditions/examples.mjs +11 -0
- package/engine/conditions/index.mjs +85 -0
- package/engine/conditions/names.mjs +21 -0
- package/engine/conditions/phase.mjs +54 -0
- package/engine/conditions/preset.mjs +94 -0
- package/engine/conditions/scope.mjs +18 -0
- package/engine/conditions/session.mjs +22 -0
- package/engine/conditions/source.mjs +63 -0
- package/engine/conditions/subject.mjs +121 -0
- package/engine/conditions/text.mjs +44 -0
- package/engine/conditions/values.mjs +32 -0
- package/engine/dev-tool-search.mjs +213 -0
- package/engine/executor.mjs +55 -6
- package/engine/fillers.mjs +23 -16
- package/engine/instruction-hint.mjs +34 -22
- package/engine/layers.mjs +135 -49
- package/engine/order.mjs +4 -1
- package/engine/predicates.mjs +2 -753
- package/engine/rule-engine.mjs +40 -0
- package/engine/rule-runtime.mjs +110 -0
- package/engine/rule-spec.mjs +187 -0
- package/engine/schema.mjs +16 -5
- package/engine/shared.mjs +1 -1
- package/engine/strategies.mjs +2 -235
- package/engine/subagent-tool-policy-core.mjs +7 -7
- package/engine/subagent-tool-policy.mjs +20 -10
- package/engine/tool-config-engine.mjs +53 -32
- package/engine/trigger-spec.mjs +49 -133
- package/lib/client.js +11117 -10860
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +82 -139
- package/lib/index.mjs +826 -1087
- package/lib/migrate-rules.d.mts +1 -0
- package/lib/migrate-rules.mjs +39 -0
- package/lib/module-package-CHPFweqK.mjs +2 -0
- package/lib/module-package-Cnd1_mwx.mjs +2872 -0
- package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
- package/lib/preset-transfer.d.mts +2 -2
- package/lib/preset-transfer.mjs +3 -2
- package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
- package/modules/ponytail/module.yml +381 -147
- package/modules/skill-surface/module.yml +74 -0
- package/modules/tool-surface/module.yml +67 -0
- package/package.json +5 -1
- package/templates/10-pre-step.yml +26 -22
- package/templates/14-first-turn-anchor.yml +25 -21
- package/templates/15-guide-auto.yml +34 -19
- package/templates/16-custom-fallback.yml +27 -15
- package/templates/18-placeholder-env-facts.yml +23 -14
- package/templates/20-system-section.yml +16 -10
- package/templates/30-runtime-context.yml +13 -7
- package/templates/40-agent-request.yml +11 -8
- package/templates/50-llm-stream.yml +14 -6
- package/templates/60-tool-pipeline.yml +15 -9
- package/templates/65-turn-stop.yml +19 -12
- package/templates/66-subagent-start.yml +12 -7
- package/templates/70-subagent-maintenance.yml +35 -29
- package/templates/75-subagent-end.yml +10 -5
- package/templates/80-tool-surface.yml +66 -0
- package/templates/policies/legacy-defaults.yml +34 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
- package/engine/prompt-config-engine.mjs +0 -104
- package/engine/trigger-editor-meta.mjs +0 -48
- package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
- package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
- package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
- package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** 独立装配入口只读取统一 rules 产物包;插件管理路径直接调用同一编译/装配接口。 */
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { basename, dirname, isAbsolute } from 'node:path'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
import { parse } from './vendor/yaml/index.js'
|
|
6
|
+
import { defineConfig, passthrough } from './fields.mjs'
|
|
7
|
+
import { compileRules, getRuleEditorMeta } from './rule-spec.mjs'
|
|
8
|
+
import { mountRuleSources } from './rule-runtime.mjs'
|
|
9
|
+
import { loadStandingMountFor } from './predicates.mjs'
|
|
10
|
+
|
|
11
|
+
export const name = 'rule-engine'
|
|
12
|
+
export const inject = ['systemPrompt', 'tools', 'llm']
|
|
13
|
+
export { compileRules, mountRuleSources, getRuleEditorMeta }
|
|
14
|
+
const fields = defineConfig({
|
|
15
|
+
rulesFile: passthrough(value => typeof value === 'string' && value.length ? value : '../rules.yml'),
|
|
16
|
+
moduleRoot: passthrough(value => typeof value === 'string' && value.length ? value : undefined),
|
|
17
|
+
presetRoot: passthrough(value => typeof value === 'string' && value.length ? value : undefined),
|
|
18
|
+
strategyDir: passthrough(value => typeof value === 'string' && value.length ? value : undefined),
|
|
19
|
+
})
|
|
20
|
+
export const configContract = { ...fields, parse(config, plugin = name) {
|
|
21
|
+
const source = fields.parse(config, plugin)
|
|
22
|
+
if (source.moduleRoot !== undefined && source.presetRoot !== undefined && source.moduleRoot !== source.presetRoot) throw new TypeError(`${plugin}: moduleRoot and presetRoot conflict`)
|
|
23
|
+
source.moduleRoot ??= source.presetRoot
|
|
24
|
+
delete source.presetRoot
|
|
25
|
+
return source
|
|
26
|
+
} }
|
|
27
|
+
|
|
28
|
+
export async function apply(ctx, config) {
|
|
29
|
+
const source = configContract.parse(config, name)
|
|
30
|
+
const file = isAbsolute(source.rulesFile) ? source.rulesFile : fileURLToPath(new URL(source.rulesFile, import.meta.url))
|
|
31
|
+
const packet = parse(readFileSync(file, 'utf8'), { logLevel: 'silent' })
|
|
32
|
+
if (packet === null || typeof packet !== 'object' || Array.isArray(packet) || !Array.isArray(packet.rules)) throw new TypeError('rule-engine: rules.yml must contain a rules package')
|
|
33
|
+
const templateBaseUrl = pathToFileURL(file)
|
|
34
|
+
const templateModuleRoot = source.moduleRoot === undefined ? new URL('./', templateBaseUrl)
|
|
35
|
+
: isAbsolute(source.moduleRoot) ? pathToFileURL(source.moduleRoot.replace(/[\\/]?$/, '/')) : new URL(source.moduleRoot.replace(/\/?$/, '/'), templateBaseUrl)
|
|
36
|
+
const strategyDir = source.strategyDir === undefined ? undefined : isAbsolute(source.strategyDir) ? pathToFileURL(source.strategyDir).href : new URL(source.strategyDir, templateBaseUrl).href
|
|
37
|
+
const moduleId = typeof packet.moduleId === 'string' ? packet.moduleId : basename(dirname(file))
|
|
38
|
+
const rules = compileRules(packet.rules, { moduleId, variables: packet.variables, variablesEnabled: packet.variablesEnabled, configOrder: packet.configOrder, ctx, standingMountFor: await loadStandingMountFor(), promptConfigOptions: { templateBaseUrl, templateModuleRoot, strategyDir } })
|
|
39
|
+
return mountRuleSources(ctx, [{ moduleId, rules }])
|
|
40
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { prepareAction } from './actions/index.mjs'
|
|
2
|
+
import { applyPromptConfigSources } from './executor.mjs'
|
|
3
|
+
import { wireLayers } from './layers.mjs'
|
|
4
|
+
import { wireTriggerObservers } from './trigger.mjs'
|
|
5
|
+
import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
|
|
6
|
+
import { createWarnOnce } from './shared.mjs'
|
|
7
|
+
|
|
8
|
+
const noNext = new Set(['agent/inbox/inserted', 'agent/turn-stopping', 'subagent/start', 'subagent/end'])
|
|
9
|
+
const compare = (a, b) => a.action.channelOrder - b.action.channelOrder
|
|
10
|
+
|| a.moduleId.localeCompare(b.moduleId) || a.ruleIndex - b.ruleIndex || a.action.actionIndex - b.action.actionIndex
|
|
11
|
+
|
|
12
|
+
/** 所有来源只进入这个装配入口;注入保留原有批处理和协调器所有权。 */
|
|
13
|
+
export function mountRuleSources(ctx, sources, options = {}) {
|
|
14
|
+
const warnOnce = options.warnOnce ?? createWarnOnce(ctx, options.plugin ?? 'rule-engine')
|
|
15
|
+
const releases = []
|
|
16
|
+
const points = new Map()
|
|
17
|
+
const injections = new Map(sources.map(source => [source.moduleId, { sourceId: `module:${source.moduleId}`, configs: [], ruleActions: [], officialInstructions: source.officialInstructions === true }]))
|
|
18
|
+
const rules = sources.flatMap(source => source.rules.filter(rule => rule.enabled !== false))
|
|
19
|
+
const observer = wireTriggerObservers(ctx, rules, { plugin: options.plugin ?? 'rule-engine', warnOnce })
|
|
20
|
+
if (observer) releases.push(observer)
|
|
21
|
+
let active = true
|
|
22
|
+
const bind = (item, on) => prepareAction(item.action, { plugin: options.plugin, warnOnce, promptConfigOptions: item.rule.promptConfigOptions, on })(ctx)
|
|
23
|
+
try {
|
|
24
|
+
for (const source of sources) for (const [ruleIndex, rule] of source.rules.entries()) {
|
|
25
|
+
if (rule.enabled === false) continue
|
|
26
|
+
for (const action of rule.actions) {
|
|
27
|
+
const item = { moduleId: source.moduleId, ruleIndex, rule, action }
|
|
28
|
+
if (action.execution.lifecycle === 'registration' && action.kind === 'guard') { releases.push(bind(item)); continue }
|
|
29
|
+
const point = action.execution
|
|
30
|
+
const key = `${point.channel}:${point.phase}:${action.waterfallPosition}`
|
|
31
|
+
const bucket = points.get(key) ?? { ...point, waterfallPosition: action.waterfallPosition, items: [] }
|
|
32
|
+
bucket.items.push(item); points.set(key, bucket)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const point of points.values()) {
|
|
36
|
+
point.items.sort(compare)
|
|
37
|
+
const handlers = []
|
|
38
|
+
let textConfigs = []
|
|
39
|
+
const flushLayers = () => {
|
|
40
|
+
if (!textConfigs.length) return
|
|
41
|
+
releases.push(wireLayers(ctx, textConfigs, warnOnce, {
|
|
42
|
+
on: (channel, handler) => {
|
|
43
|
+
if (channel !== point.channel) throw new TypeError(`rule action registered outside ${point.channel}: ${channel}`)
|
|
44
|
+
handlers.push({ handler }); return () => {}
|
|
45
|
+
},
|
|
46
|
+
}))
|
|
47
|
+
textConfigs = []
|
|
48
|
+
}
|
|
49
|
+
for (const item of point.items) {
|
|
50
|
+
const { action, rule, moduleId } = item
|
|
51
|
+
if (action.kind === 'inject-text') {
|
|
52
|
+
const config = { ...action.compiledConfig, rule, sourceModuleId: moduleId, group: undefined, exclusive: false, enabled: true, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen }
|
|
53
|
+
if (config.layer === 'pre-step') injections.get(moduleId).configs.push(config)
|
|
54
|
+
else textConfigs.push(config)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
flushLayers()
|
|
58
|
+
const on = (channel, handler) => {
|
|
59
|
+
if (channel !== point.channel) throw new TypeError(`rule action registered outside ${point.channel}: ${channel}`)
|
|
60
|
+
const entry = { rule, handler, sequence: action.channelOrder, sourceModuleId: moduleId, ruleId: rule.id, ruleActionIndex: action.actionIndex, id: action.id, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen }
|
|
61
|
+
if (channel === 'agent/pre-step' && point.waterfallPosition === 'default') injections.get(moduleId).ruleActions.push(entry)
|
|
62
|
+
else handlers.push(entry)
|
|
63
|
+
return () => {}
|
|
64
|
+
}
|
|
65
|
+
releases.push(bind(item, on))
|
|
66
|
+
}
|
|
67
|
+
flushLayers()
|
|
68
|
+
if (!handlers.length) continue
|
|
69
|
+
releases.push(ctx.on(point.channel, (...args) => {
|
|
70
|
+
const free = noNext.has(point.channel)
|
|
71
|
+
const payload = free ? args : args.slice(0, -1)
|
|
72
|
+
const next = free ? () => undefined : args[args.length - 1]
|
|
73
|
+
if (!active) return next()
|
|
74
|
+
const frame = ruleFrame(point.channel, payload, warnOnce, ctx)
|
|
75
|
+
if (free) {
|
|
76
|
+
let pending
|
|
77
|
+
for (const entry of handlers) {
|
|
78
|
+
const invoke = () => active && actionMatches(entry, frame) ? entry.handler(...payload, undefined, frame) : undefined
|
|
79
|
+
pending = pending?.then ? pending.then(invoke) : invoke()
|
|
80
|
+
}
|
|
81
|
+
return pending
|
|
82
|
+
}
|
|
83
|
+
if (point.phase === 'after-next') return Promise.resolve(next()).then(async initial => {
|
|
84
|
+
let result = initial
|
|
85
|
+
for (const entry of handlers) {
|
|
86
|
+
if (!active || !actionMatches(entry, frame)) continue
|
|
87
|
+
result = await entry.handler(...payload, () => result, frame)
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
})
|
|
91
|
+
const invoke = index => {
|
|
92
|
+
if (!active || index === handlers.length) return next()
|
|
93
|
+
const entry = handlers[index]
|
|
94
|
+
return actionMatches(entry, frame) ? entry.handler(...payload, () => invoke(index + 1), frame) : invoke(index + 1)
|
|
95
|
+
}
|
|
96
|
+
return invoke(0)
|
|
97
|
+
}, point.waterfallPosition === 'outermost' ? { prepend: true } : undefined))
|
|
98
|
+
}
|
|
99
|
+
const preStep = [...injections.values()].filter(source => source.configs.length || source.ruleActions.length)
|
|
100
|
+
if (preStep.length) releases.push(applyPromptConfigSources(ctx, preStep, { layers: false }))
|
|
101
|
+
} catch (error) {
|
|
102
|
+
active = false
|
|
103
|
+
for (const dispose of releases.reverse()) { try { dispose?.() } catch {} }
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
return () => {
|
|
107
|
+
active = false
|
|
108
|
+
for (const dispose of releases.splice(0).reverse()) { try { dispose?.() } catch {} }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** module.yml.rules 的唯一运行时编译入口。旧声明仅由离线迁移器读取。 */
|
|
2
|
+
import { ACTION_KINDS, actionExecutionPoint, prepareAction, validateActionOptions } from './actions/index.mjs'
|
|
3
|
+
import { compileWhen, PREDICATE_FACTORIES, COMPOSITE_OPERATORS } from './conditions/index.mjs'
|
|
4
|
+
import { createPromptConfigs, KNOWN_LAYERS } from './schema.mjs'
|
|
5
|
+
import { WATERFALL_POSITIONS } from './trigger.mjs'
|
|
6
|
+
import { ACTION_EXAMPLES } from './actions/examples.mjs'
|
|
7
|
+
import { PREDICATE_EXAMPLES } from './conditions/examples.mjs'
|
|
8
|
+
import { conjunction, expandActions } from './branch.mjs'
|
|
9
|
+
|
|
10
|
+
/** 规则字段:`if`/`then`/`else` 是当前名,`when`/`do` 保留为兼容输入(见 pickAlias)。 */
|
|
11
|
+
const RULE_FIELDS = new Set(['id', 'name', 'enabled', 'layer', 'group', 'exclusive', 'if', 'then', 'else', 'when', 'do'])
|
|
12
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
const nonempty = value => typeof value === 'string' && value.trim().length > 0
|
|
14
|
+
|
|
15
|
+
/** 规则及物化身份的唯一安全边界;host 保存、导入与直接编译共用。 */
|
|
16
|
+
export function assertRuleId(id) {
|
|
17
|
+
if (!nonempty(id)) throw new TypeError('rule id must be a non-empty string')
|
|
18
|
+
if (id === '.' || id === '..') throw new TypeError(`rule id ${JSON.stringify(id)} must not be a dot directory`)
|
|
19
|
+
const reserved = '/\\\0:*?"<>|'
|
|
20
|
+
for (const char of reserved) if (id.includes(char)) throw new TypeError(`rule id ${JSON.stringify(id)} contains path separators or reserved filename characters`)
|
|
21
|
+
for (let index = 0; index < id.length; index++) if (id.charCodeAt(index) < 0x20) throw new TypeError(`rule id ${JSON.stringify(id)} contains control characters`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 注入叶子的唯一身份投影;稳定动作 id 保证重排不改变投递身份。 */
|
|
25
|
+
export function injectionConfigSpec(rule, action, options = {}) {
|
|
26
|
+
if (action?.kind !== 'inject-text' || !record(action.config)) throw new TypeError('inject-text config must be an object')
|
|
27
|
+
const config = { ...action.config, id: action.config.id ?? `rule:${encodeURIComponent(rule.id)}:${encodeURIComponent(action.id)}`, layer: action.config.layer ?? rule.layer ?? 'pre-step' }
|
|
28
|
+
const variables = record(options.variables) ? options.variables : {}
|
|
29
|
+
if (options.variablesEnabled === false) {
|
|
30
|
+
const keys = new Set(Object.keys(variables))
|
|
31
|
+
const strip = text => typeof text === 'string' ? text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (match, key) => keys.has(key) ? '' : match) : text
|
|
32
|
+
if (config.text !== undefined) config.text = strip(config.text)
|
|
33
|
+
if (Array.isArray(config.texts)) config.texts = config.texts.map(strip)
|
|
34
|
+
if (record(config.params) && typeof config.params.text === 'string') config.params = { ...config.params, text: strip(config.params.text) }
|
|
35
|
+
} else config.variables = { ...variables, ...(record(config.variables) ? config.variables : {}) }
|
|
36
|
+
return config
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 非空同组任一成员声明互斥,整组最多允许一条 enabled;运行时不替作者选赢家。 */
|
|
40
|
+
export function validateRuleGroups(rules) {
|
|
41
|
+
const groups = new Map()
|
|
42
|
+
for (const rule of rules) {
|
|
43
|
+
if (!nonempty(rule.group)) continue
|
|
44
|
+
const group = groups.get(rule.group) ?? []; group.push(rule); groups.set(rule.group, group)
|
|
45
|
+
}
|
|
46
|
+
for (const [name, group] of groups) {
|
|
47
|
+
if (group.some(rule => rule.exclusive === true) && group.filter(rule => rule.enabled !== false).length > 1) {
|
|
48
|
+
throw new TypeError(`rules: exclusive group ${JSON.stringify(name)} has multiple enabled rules`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ruleActionExecution(action) {
|
|
54
|
+
const fixed = action.kind === 'guard' || (action.kind === 'inject-text'
|
|
55
|
+
&& (action.config?.params?.complete === true || action.config?.params?.suppressRuntimeContext === true))
|
|
56
|
+
return { ...actionExecutionPoint(action), lifecycle: fixed ? 'registration' : 'event' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 通用门只属于 when;固定系统段的 audience 保留为注册目标。 */
|
|
60
|
+
function normalizeActionGates(action, execution) {
|
|
61
|
+
const inject = action.kind === 'inject-text'
|
|
62
|
+
if (!inject && action.kind !== 'request-params') return
|
|
63
|
+
const target = inject ? action.config : action
|
|
64
|
+
if (inject) {
|
|
65
|
+
if (target.enabled !== undefined && target.enabled !== true) throw new TypeError(`action ${action.id}: move config.enabled to rule.enabled`)
|
|
66
|
+
delete target.enabled
|
|
67
|
+
}
|
|
68
|
+
const fields = inject ? ['audience', 'modelScope', 'promotion', 'subject', 'match'] : ['audience', 'modelScope']
|
|
69
|
+
for (const field of fields) {
|
|
70
|
+
const value = target[field]
|
|
71
|
+
if (inject && field === 'audience' && target.layer === 'system-section' && execution.lifecycle === 'registration' && ['main', 'subagent'].includes(value)) continue
|
|
72
|
+
const neutral = value == null || value === '' || (field === 'modelScope' && value === 'all') || (field === 'promotion' && value === 'none')
|
|
73
|
+
if (!neutral) throw new TypeError(`action ${action.id}: ${inject ? 'config.' : ''}${field} is a legacy gate; move it to rule.if`)
|
|
74
|
+
delete target[field]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 注册制层:内容在 agent 装配时注册一次,没有逐轮求值时机,因此不接受分支条件。 */
|
|
79
|
+
const REGISTRATION_LAYERS = new Set(['system-section', 'runtime-context'])
|
|
80
|
+
|
|
81
|
+
/** options.promptConfigOptions 拥有模板/策略路径;configOrder 使用规则 id,moduleId 只标记来源。 */
|
|
82
|
+
export function compileRules(specs, options = {}) {
|
|
83
|
+
if (!Array.isArray(specs)) throw new TypeError('rules must be an array')
|
|
84
|
+
const ids = new Set()
|
|
85
|
+
const pendingConfigs = []
|
|
86
|
+
const moduleId = options.moduleId ?? options.sourceModuleId
|
|
87
|
+
const rules = specs.map((spec, index) => {
|
|
88
|
+
if (!record(spec)) throw new TypeError(`rules[${index}] must be an object`)
|
|
89
|
+
const unknown = Object.keys(spec).filter(key => !RULE_FIELDS.has(key))
|
|
90
|
+
if (unknown.length) throw new TypeError(`rules[${index}] unknown fields: ${unknown.join(', ')}`)
|
|
91
|
+
assertRuleId(spec.id)
|
|
92
|
+
if (ids.has(spec.id)) throw new TypeError(`rules: duplicate id ${spec.id}`)
|
|
93
|
+
ids.add(spec.id)
|
|
94
|
+
for (const key of ['enabled', 'exclusive']) if (spec[key] !== undefined && typeof spec[key] !== 'boolean') throw new TypeError(`rule ${spec.id}.${key} must be boolean`)
|
|
95
|
+
for (const key of ['name', 'group']) if (spec[key] !== undefined && typeof spec[key] !== 'string') throw new TypeError(`rule ${spec.id}.${key} must be string`)
|
|
96
|
+
if (spec.layer !== undefined && !KNOWN_LAYERS.has(spec.layer)) throw new TypeError(`rule ${spec.id}: unknown layer ${spec.layer}`)
|
|
97
|
+
// 旧名已按引擎重构退役:显式拒绝并给出新名,比 "unknown fields" 更可直接照做。
|
|
98
|
+
if (spec.when !== undefined) throw new TypeError(`rule ${spec.id}: "when" 已退役,改用 "if"`)
|
|
99
|
+
if (spec.do !== undefined) throw new TypeError(`rule ${spec.id}: "do" 已退役,改用 "then"`)
|
|
100
|
+
const ruleIf = spec.if
|
|
101
|
+
const ruleThen = spec.then
|
|
102
|
+
if (!Array.isArray(ruleThen) || ruleThen.length === 0) throw new TypeError(`rule ${spec.id}.then must be a non-empty array`)
|
|
103
|
+
const sequence = options.configOrder?.[spec.id] ?? index * 10
|
|
104
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) throw new TypeError(`rule ${spec.id}: invalid configOrder`)
|
|
105
|
+
const when = compileWhen(ruleIf, options)
|
|
106
|
+
// 规则级 `if` 由 `rule.when` 判定,then 分支的动作不再重复叠加它;规则级 `else` 的动作
|
|
107
|
+
// 自带 `not(if)` 并标记跳过规则级判定——否则两者自相矛盾,else 分支永远不会执行。
|
|
108
|
+
const expanded = [
|
|
109
|
+
...expandActions(ruleThen, {}, `rule ${spec.id}.then`),
|
|
110
|
+
...(spec.else === undefined ? [] : expandActions(spec.else, {
|
|
111
|
+
outer: ruleIf === undefined ? [] : [{ not: ruleIf }],
|
|
112
|
+
bypass: true,
|
|
113
|
+
}, `rule ${spec.id}.else`)),
|
|
114
|
+
]
|
|
115
|
+
const actionIds = new Set()
|
|
116
|
+
const actions = expanded.map(({ node: source, conditions, bypass }, actionIndex) => {
|
|
117
|
+
if (!record(source) || !nonempty(source.id)) throw new TypeError(`rule ${spec.id}: action id must be non-empty`)
|
|
118
|
+
if (actionIds.has(source.id)) throw new TypeError(`rule ${spec.id}: duplicate action id ${source.id}`)
|
|
119
|
+
actionIds.add(source.id)
|
|
120
|
+
if (!Object.hasOwn(ACTION_KINDS, source.kind)) throw new TypeError(`rule ${spec.id}: unknown action ${source.kind}`)
|
|
121
|
+
const action = structuredClone(source)
|
|
122
|
+
if (action.kind === 'inject-text') {
|
|
123
|
+
action.config = injectionConfigSpec(spec, action, options)
|
|
124
|
+
if (action.config.strategy === 'custom-fallback') throw new TypeError(`action ${action.id}: migrate custom-fallback to an anchor condition and anchor-notice content`)
|
|
125
|
+
}
|
|
126
|
+
const execution = ruleActionExecution(action)
|
|
127
|
+
normalizeActionGates(action, execution)
|
|
128
|
+
const channelOrder = action.channelOrder ?? sequence
|
|
129
|
+
if (!Number.isSafeInteger(channelOrder) || channelOrder < 0) throw new TypeError(`action ${action.id}: channelOrder must be a non-negative safe integer`)
|
|
130
|
+
const waterfallPosition = action.waterfallPosition ?? 'default'
|
|
131
|
+
if (!WATERFALL_POSITIONS.has(waterfallPosition)) throw new TypeError(`action ${action.id}: invalid waterfallPosition`)
|
|
132
|
+
if (execution.lifecycle === 'registration' && (when !== undefined || waterfallPosition !== 'default')) throw new TypeError(`action ${action.id}: registration effects do not support when or waterfall positioning`)
|
|
133
|
+
if (action.kind === 'inject-text' && waterfallPosition !== 'default') throw new TypeError(`action ${action.id}: inject-text placement belongs to its official layer, not waterfallPosition`)
|
|
134
|
+
if (action.kind === 'inject-text') {
|
|
135
|
+
validateActionOptions(action)
|
|
136
|
+
if (!record(action.config)) throw new TypeError(`action ${action.id}: config must be an object`)
|
|
137
|
+
// 原始声明保持不变;稳定动作身份只在未提供正文身份时补入运行时编译输入。
|
|
138
|
+
pendingConfigs.push({ action, source: { ...action.config, id: action.config.id ?? `${spec.id}:${action.id}` }, sequence: channelOrder, ruleId: spec.id, actionIndex })
|
|
139
|
+
} else prepareAction(action, { promptConfigOptions: options.promptConfigOptions })
|
|
140
|
+
// 空条件数组经 conjunction → undefined → compileWhen 直接返回 undefined(无条件动作)。
|
|
141
|
+
const actionWhen = compileWhen(conjunction(conditions), options)
|
|
142
|
+
const branchLayer = action.kind === 'inject-text' ? action.config.layer : spec.layer ?? 'pre-step'
|
|
143
|
+
if (actionWhen !== undefined && (REGISTRATION_LAYERS.has(branchLayer) || execution.lifecycle === 'registration')) {
|
|
144
|
+
throw new TypeError(`action ${action.id}: layer ${branchLayer} has no per-turn evaluation point — move the branch to pre-step / subagent-* / tool-pipeline / turn-stop`)
|
|
145
|
+
}
|
|
146
|
+
return Object.assign(action, { execution, channelOrder, waterfallPosition, actionIndex, actionWhen, bypassRuleWhen: bypass })
|
|
147
|
+
})
|
|
148
|
+
const pointOrders = new Map()
|
|
149
|
+
for (const action of actions) {
|
|
150
|
+
const key = `${action.execution.channel}:${action.execution.phase}:${action.waterfallPosition}`
|
|
151
|
+
const previous = pointOrders.get(key)
|
|
152
|
+
if (previous !== undefined && previous.channelOrder !== action.channelOrder) throw new TypeError(`rule ${spec.id}: conflicting channelOrder for actions ${previous.id} and ${action.id} at ${key}`)
|
|
153
|
+
pointOrders.set(key, action)
|
|
154
|
+
}
|
|
155
|
+
return { id: spec.id, name: spec.name, layer: spec.layer, group: spec.group, exclusive: spec.exclusive === true, enabled: spec.enabled !== false, sequence, when, actions, promptConfigOptions: options.promptConfigOptions }
|
|
156
|
+
})
|
|
157
|
+
validateRuleGroups(rules)
|
|
158
|
+
const complete = rules.filter(rule => rule.enabled).flatMap(rule => rule.actions).filter(action => action.config?.params?.complete === true)
|
|
159
|
+
if (complete.length > 1 || (complete.length > 0 && options.personaComplete === true)) throw new TypeError('rules: multiple complete system sections are active')
|
|
160
|
+
if (pendingConfigs.length) {
|
|
161
|
+
const compiled = createPromptConfigs(pendingConfigs.map(item => item.source), { ...options.promptConfigOptions, ...(moduleId === undefined ? {} : { sourceModuleId: moduleId }) })
|
|
162
|
+
const byId = new Map(compiled.map(config => [config.id, config]))
|
|
163
|
+
for (const item of pendingConfigs) {
|
|
164
|
+
const config = byId.get(item.source.id)
|
|
165
|
+
Object.assign(config, { sequence: item.sequence, ruleId: item.ruleId, ruleActionIndex: item.actionIndex })
|
|
166
|
+
item.action.compiledConfig = config
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return rules
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getRuleEditorMeta() {
|
|
173
|
+
const predicates = Object.keys(PREDICATE_FACTORIES).map(kind => ({ kind, example: { [kind]: structuredClone(PREDICATE_EXAMPLES[kind]) } }))
|
|
174
|
+
for (const kind of COMPOSITE_OPERATORS) {
|
|
175
|
+
const child = { phase: { promoted: false } }
|
|
176
|
+
predicates.push({ kind, example: { [kind]: kind === 'not' ? child : [child] } })
|
|
177
|
+
}
|
|
178
|
+
for (const { example } of predicates) compileWhen(example)
|
|
179
|
+
const actions = Object.keys(ACTION_KINDS).map(kind => {
|
|
180
|
+
const example = { kind, ...structuredClone(ACTION_EXAMPLES[kind]) }
|
|
181
|
+
// 新规则的投递身份由稳定 rule/action id 派生;迁移的显式 config.id 仍保留。
|
|
182
|
+
if (kind === 'inject-text') delete example.config.id
|
|
183
|
+
const action = compileRules([{ id: kind, then: [{ ...example, id: 'example' }] }])[0].actions[0]
|
|
184
|
+
return { kind, example, channel: action.execution.channel, phase: action.execution.phase, lifecycle: action.execution.lifecycle, supportsWhen: action.execution.lifecycle === 'event' }
|
|
185
|
+
})
|
|
186
|
+
return { predicates, actions, composites: [...COMPOSITE_OPERATORS], waterfallPositions: [...WATERFALL_POSITIONS] }
|
|
187
|
+
}
|
package/engine/schema.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { attachStRenderers } from './st-render.mjs'
|
|
|
12
12
|
import { FILE_SEQUENCE, compareConfigSequence } from './order.mjs'
|
|
13
13
|
import { MATCH_LOGIC, createAnchorMatcher } from './anchor-match.mjs'
|
|
14
14
|
|
|
15
|
-
const name = '
|
|
15
|
+
const name = 'rule-engine'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 内容模板加载:提示词配置可声明 templateFile,由外部 yml / json / 纯文本模板提供内容,
|
|
@@ -146,7 +146,7 @@ export function loadPromptConfigFiles(dirUrl) {
|
|
|
146
146
|
return specs
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
export const KNOWN_STRATEGIES = new Set(['static', 'placeholder', 'first-turn-anchor', 'guide-auto', '
|
|
149
|
+
export const KNOWN_STRATEGIES = new Set(['static', 'placeholder', 'first-turn-anchor', 'guide-auto', 'anchor-notice', 'world-book'])
|
|
150
150
|
/**
|
|
151
151
|
* 策略 × 层支持矩阵:`config.resolve` 只在 pre-step(executor)与 runtime-context 的
|
|
152
152
|
* placeholder(layers)被消费,其他层声明非 static 策略会绑定 resolver 却无人调用,
|
|
@@ -158,7 +158,7 @@ export const STRATEGY_LAYER_SUPPORT = {
|
|
|
158
158
|
placeholder: ['pre-step', 'runtime-context'],
|
|
159
159
|
'first-turn-anchor': ['pre-step'],
|
|
160
160
|
'guide-auto': ['pre-step'],
|
|
161
|
-
'
|
|
161
|
+
'anchor-notice': ['pre-step'],
|
|
162
162
|
'world-book': ['pre-step'],
|
|
163
163
|
}
|
|
164
164
|
/** 模板专属策略允许的层:resolver 同样只在 pre-step / runtime-context 被调用。 */
|
|
@@ -528,6 +528,9 @@ function resolveConfigFields(spec, label, layer) {
|
|
|
528
528
|
|
|
529
529
|
/** 从 YAML 提示词配置描述构造运行时提示词配置。配置错误必须在挂载时暴露(fail loud)。 */
|
|
530
530
|
export function createPromptConfigs(specs, options = {}) {
|
|
531
|
+
if (options.templateModuleRoot !== undefined && options.templatePresetRoot !== undefined
|
|
532
|
+
&& String(options.templateModuleRoot) !== String(options.templatePresetRoot)) throw new TypeError('templateModuleRoot and templatePresetRoot conflict')
|
|
533
|
+
const templateModuleRoot = options.templateModuleRoot ?? options.templatePresetRoot
|
|
531
534
|
if (specs === undefined) return []
|
|
532
535
|
if (!Array.isArray(specs)) throw new TypeError(`${name}: config.configs must be an array`)
|
|
533
536
|
// 重复 ID 拒绝:后者覆盖前者会静默丢卡,挂载前 fail loud。
|
|
@@ -550,6 +553,7 @@ export function createPromptConfigs(specs, options = {}) {
|
|
|
550
553
|
}
|
|
551
554
|
const rawStrategy = spec.strategy ?? 'static'
|
|
552
555
|
const strategy = rawStrategy
|
|
556
|
+
if (strategy === 'custom-fallback') throw new TypeError(`${name}: migrate custom-fallback to an anchor condition and anchor-notice content`)
|
|
553
557
|
if (!KNOWN_STRATEGIES.has(strategy)) {
|
|
554
558
|
// 模板专属策略:声明了 strategyDir 时由 strategies.bindResolver 懒加载,
|
|
555
559
|
// 否则视为未知策略 fail loud。
|
|
@@ -591,7 +595,7 @@ export function createPromptConfigs(specs, options = {}) {
|
|
|
591
595
|
}
|
|
592
596
|
}
|
|
593
597
|
// 安装预检可把文件读取定向到尚未提交的候选目录,默认运行期仍走原解析器。
|
|
594
|
-
const template = (options.loadTemplate ?? loadTemplate)(spec.templateFile, options.templateBaseUrl,
|
|
598
|
+
const template = (options.loadTemplate ?? loadTemplate)(spec.templateFile, options.templateBaseUrl, templateModuleRoot)
|
|
595
599
|
const templatePatch = template !== null && typeof template === 'object'
|
|
596
600
|
? { id: template.id, role: template.role, content: template.content, source: template.source }
|
|
597
601
|
: undefined
|
|
@@ -618,7 +622,14 @@ export function createPromptConfigs(specs, options = {}) {
|
|
|
618
622
|
modelScope: fields.modelScope,
|
|
619
623
|
subject: fields.subject,
|
|
620
624
|
match,
|
|
621
|
-
|
|
625
|
+
// sourceKind 进的是消息 source.kind,必须是 **v4 的生产者身份**(`plugin:<owner>`):
|
|
626
|
+
// 裸规则 id 会被 `userMessagesText` 的注入过滤漏掉(它只认 `source.plugin` 与
|
|
627
|
+
// `plugin:` 前缀),于是本引擎注入过的规则正文会回来参与下一轮 when 判定——实测
|
|
628
|
+
// 后果是只读档正文里的「只读」二字让写档的 notAny 在第二轮翻转,同一子代理两档并注。
|
|
629
|
+
sourceKind: (() => {
|
|
630
|
+
const declared = typeof spec.sourceKind === 'string' && spec.sourceKind.length > 0 ? spec.sourceKind : spec.id
|
|
631
|
+
return declared.startsWith('plugin:') ? declared : `plugin:${declared}`
|
|
632
|
+
})(),
|
|
622
633
|
form: typeof spec.form === 'string' ? spec.form : 'notice',
|
|
623
634
|
summary: typeof spec.summary === 'string' ? spec.summary : '',
|
|
624
635
|
identity: fields.identity,
|
package/engine/shared.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export function isFlashModel(modelId) {
|
|
|
115
115
|
|
|
116
116
|
/** 模型范围过滤:flash=仅 Flash 家族模型;pro=仅非 Flash;all=全部。 */
|
|
117
117
|
export function matchesModel(scope, model) {
|
|
118
|
-
if (scope === 'all') return true
|
|
118
|
+
if (scope == null || scope === 'all') return true
|
|
119
119
|
const isFlash = isFlashModel(model)
|
|
120
120
|
return scope === 'flash' ? isFlash : !isFlash
|
|
121
121
|
}
|