dsh-plugin-prompt-tool 3.1.1 → 3.1.3
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/engine/layers.mjs +108 -13
- package/engine/rule-runtime.d.mts +6 -0
- package/engine/rule-runtime.mjs +233 -95
- package/lib/client.js +367 -367
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +884 -329
- package/package.json +1 -1
package/engine/layers.mjs
CHANGED
|
@@ -71,7 +71,7 @@ function officialAliasOf(name) {
|
|
|
71
71
|
* 非法官方名(中文/大写/连字符)改写成确定性别名并改写文本引用;注册失败的名字退回静态解析。
|
|
72
72
|
* @returns 各配置独立的引用白名单与别名;同一作用域内等价绑定只注册一次。
|
|
73
73
|
*/
|
|
74
|
-
function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
74
|
+
function registerOfficialVariables(ctx, configs, warnOnce, keep, shared) {
|
|
75
75
|
const registries = new Map(configs.map((config) => [config, { protect: new Set(), alias: new Map(), registered: new Set() }]))
|
|
76
76
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
77
77
|
if (systemPrompt === undefined || typeof systemPrompt.variable !== 'function') return registries
|
|
@@ -92,8 +92,9 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
for (const fact of facts) reserved.add(fact)
|
|
95
|
-
const bindings = new Map()
|
|
96
|
-
const usedNames = new Set()
|
|
95
|
+
const bindings = shared?.bindings ?? new Map()
|
|
96
|
+
const usedNames = shared?.usedNames ?? new Set()
|
|
97
|
+
const retained = new Set()
|
|
97
98
|
for (const [config, referenced] of references) {
|
|
98
99
|
const registry = registries.get(config)
|
|
99
100
|
for (const name of referenced) {
|
|
@@ -115,20 +116,30 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
|
115
116
|
usedNames.add(official)
|
|
116
117
|
const source = isFact ? name.toLowerCase() : name
|
|
117
118
|
const variablesDisabled = config[SESSION_VARIABLES_DISABLED] === true
|
|
118
|
-
|
|
119
|
+
const release = systemPrompt.variable(official, (context) => {
|
|
119
120
|
const session = context?.agent?.session
|
|
120
121
|
const variables = variablesDisabled ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables
|
|
121
122
|
const value = Object.hasOwn(variables, source) ? String(variables[source]) : runtimeFactValue(source, session) ?? ''
|
|
122
123
|
// 变量值内的 {{pick}} 同样要有位置身份(配置 id + 声明名),否则同会话下多个变量的首个 pick 撞值。
|
|
123
124
|
const expanded = interpolateVariables(value, variables, session, undefined, `variable:${config.sourceModuleId}:${config.id}:${name}`)
|
|
124
125
|
return officialChannelText(expanded, `variable ${official}`, { alias: new Map(), registered: new Set() }, warnOnce)
|
|
125
|
-
})
|
|
126
|
-
binding = official
|
|
126
|
+
})
|
|
127
|
+
binding = { name: official, release, refs: 0 }
|
|
127
128
|
bindings.set(signature, binding)
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
+
if (!retained.has(signature)) {
|
|
131
|
+
retained.add(signature)
|
|
132
|
+
binding.refs++
|
|
133
|
+
keep(() => {
|
|
134
|
+
if (--binding.refs > 0) return
|
|
135
|
+
bindings.delete(signature)
|
|
136
|
+
usedNames.delete(binding.name)
|
|
137
|
+
binding.release?.()
|
|
138
|
+
}, `${name}: official prompt variable ${binding.name}`)
|
|
139
|
+
}
|
|
140
|
+
registry.registered.add(binding.name)
|
|
130
141
|
registry.protect.add(name)
|
|
131
|
-
if (binding !== name) registry.alias.set(name, binding)
|
|
142
|
+
if (binding.name !== name) registry.alias.set(name, binding.name)
|
|
132
143
|
} catch (error) {
|
|
133
144
|
warnOnce(`${name}: 官方 prompt 变量 ${official} 注册失败,该引用退回静态解析:${String(error?.message ?? error)}`)
|
|
134
145
|
}
|
|
@@ -176,6 +187,7 @@ function textLayerGroups(configs) {
|
|
|
176
187
|
&& config.sourceModuleId === base.sourceModuleId
|
|
177
188
|
&& (config.params?.complete === true) === (base.params?.complete === true)
|
|
178
189
|
&& (config.params?.suppressRuntimeContext === true) === (base.params?.suppressRuntimeContext === true)
|
|
190
|
+
&& config.runtimePoint?.waterfallPosition === base.runtimePoint?.waterfallPosition
|
|
179
191
|
&& config.position === base.position && config.order === base.order) previous.push(config)
|
|
180
192
|
else groups.push([config])
|
|
181
193
|
}
|
|
@@ -658,9 +670,9 @@ function orderedNamesOf(entries) {
|
|
|
658
670
|
* 回填到它们原来的槽位。只换本插件段之间的位置:平台段与其他插件段占住的槽位不动。
|
|
659
671
|
*/
|
|
660
672
|
function registerDeliveryOrder(ctx, keep, collection, names) {
|
|
661
|
-
if (names.length < 2) return
|
|
662
|
-
const rank = new Map(names.map((entryName, index) => [entryName, index]))
|
|
673
|
+
if (typeof names !== 'function' && names.length < 2) return
|
|
663
674
|
keep(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
675
|
+
const rank = new Map((typeof names === 'function' ? names() : names).map((entryName, index) => [entryName, index]))
|
|
664
676
|
const result = await next()
|
|
665
677
|
const list = result?.[collection]
|
|
666
678
|
if (!Array.isArray(list)) return result
|
|
@@ -712,8 +724,8 @@ export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
|
712
724
|
try {
|
|
713
725
|
// 注册即纳入 owned,后续注册失败也能释放已取得的变量和监听器。
|
|
714
726
|
/** 实际注册名与序号:重排器只认这里,不从 configs 反推。 */
|
|
715
|
-
const ordered = { sections: [], contexts: [] }
|
|
716
|
-
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep)
|
|
727
|
+
const ordered = options.ordered ?? { sections: [], contexts: [] }
|
|
728
|
+
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep, options.sharedVariables)
|
|
717
729
|
const conditional = configs.filter(config => ['system-section', 'runtime-context'].includes(config.layer)
|
|
718
730
|
&& (typeof config.rule?.when === 'function' || typeof config.actionWhen === 'function'
|
|
719
731
|
|| (config.rule !== undefined || config.params?.templateEvaluation === true) && config.params?.complete !== true && config.params?.suppressRuntimeContext !== true))
|
|
@@ -724,7 +736,7 @@ export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
|
724
736
|
wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on, options.turnStopBudgets)
|
|
725
737
|
wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on)
|
|
726
738
|
// 官方 order 只提供初始槽位;configOrder 覆盖段/上下文的先后(平台段与其他插件段不动)。
|
|
727
|
-
for (const collection of ['sections', 'contexts']) {
|
|
739
|
+
for (const collection of options.deliveryOrder === false ? [] : ['sections', 'contexts']) {
|
|
728
740
|
registerDeliveryOrder(ctx, keep, collection, orderedNamesOf(ordered[collection]))
|
|
729
741
|
}
|
|
730
742
|
return dispose
|
|
@@ -733,3 +745,86 @@ export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
|
733
745
|
throw error
|
|
734
746
|
}
|
|
735
747
|
}
|
|
748
|
+
|
|
749
|
+
/** 按实际文本组保留注册;共享排序投影变化不撤销未变化组的 provider。 */
|
|
750
|
+
export function createLayerRuntime(ctx, warnOnce, options = {}) {
|
|
751
|
+
let records = []
|
|
752
|
+
let active = true
|
|
753
|
+
const listeners = []
|
|
754
|
+
const sharedVariables = { bindings: new Map(), usedNames: new Set() }
|
|
755
|
+
const keep = release => { listeners.push(release) }
|
|
756
|
+
try {
|
|
757
|
+
for (const collection of ['sections', 'contexts']) {
|
|
758
|
+
registerDeliveryOrder(ctx, keep, collection, () => orderedNamesOf(records.flatMap(record => record.ordered[collection])))
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
for (const release of listeners.reverse()) release()
|
|
762
|
+
throw error
|
|
763
|
+
}
|
|
764
|
+
const groupsOf = configs => {
|
|
765
|
+
const groups = []
|
|
766
|
+
for (const layer of ['system-section', 'runtime-context']) {
|
|
767
|
+
const selected = configs.filter(config => config.layer === layer)
|
|
768
|
+
const conditional = selected.filter(config => typeof config.rule?.when === 'function' || typeof config.actionWhen === 'function'
|
|
769
|
+
|| (config.rule !== undefined || config.params?.templateEvaluation === true) && config.params?.complete !== true && config.params?.suppressRuntimeContext !== true)
|
|
770
|
+
const conditionalSet = new Set(conditional)
|
|
771
|
+
const regular = selected.filter(config => !conditionalSet.has(config))
|
|
772
|
+
groups.push(...textLayerGroups(conditional))
|
|
773
|
+
if (layer === 'system-section') groups.push(...textLayerGroups(regular))
|
|
774
|
+
else {
|
|
775
|
+
const resolver = regular.filter(config => config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
|
|
776
|
+
const resolverSet = new Set(resolver)
|
|
777
|
+
groups.push(...textLayerGroups(regular.filter(config => !resolverSet.has(config))), ...resolver.map(config => [config]))
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const other = configs.filter(config => !['system-section', 'runtime-context'].includes(config.layer))
|
|
781
|
+
return [...groups, ...other.map(config => [config])]
|
|
782
|
+
}
|
|
783
|
+
const mount = configs => {
|
|
784
|
+
const ordered = { sections: [], contexts: [] }
|
|
785
|
+
const handlers = []
|
|
786
|
+
const dispose = wireLayers(ctx, configs, warnOnce, {
|
|
787
|
+
...options, sharedVariables, ordered, deliveryOrder: false,
|
|
788
|
+
on(channel, handler) { handlers.push({ channel, handler, config: configs[0] }); return () => {} },
|
|
789
|
+
})
|
|
790
|
+
return { configs, ordered, handlers, dispose }
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
replace(configs) {
|
|
794
|
+
if (!active) throw new Error('rule text runtime disposed')
|
|
795
|
+
const groups = groupsOf(configs)
|
|
796
|
+
const byFirst = new Map(records.map(record => [record.configs[0], record]))
|
|
797
|
+
const retained = groups.map(group => {
|
|
798
|
+
const record = byFirst.get(group[0])
|
|
799
|
+
return record?.configs.length === group.length && group.every((config, index) => record.configs[index] === config) ? record : undefined
|
|
800
|
+
})
|
|
801
|
+
const retainedSet = new Set(retained)
|
|
802
|
+
const removed = records.filter(record => !retainedSet.has(record))
|
|
803
|
+
for (const record of removed) record.dispose()
|
|
804
|
+
const created = []
|
|
805
|
+
try {
|
|
806
|
+
records = groups.map((group, index) => {
|
|
807
|
+
if (retained[index] !== undefined) return retained[index]
|
|
808
|
+
const record = mount(group)
|
|
809
|
+
created.push(record)
|
|
810
|
+
return record
|
|
811
|
+
})
|
|
812
|
+
const owners = new Set(configs.map(config => config.sourceModuleId ?? ''))
|
|
813
|
+
for (const owner of options.turnStopBudgets?.keys() ?? []) if (!owners.has(owner)) options.turnStopBudgets.delete(owner)
|
|
814
|
+
} catch (error) {
|
|
815
|
+
for (const record of created.reverse()) record.dispose()
|
|
816
|
+
const old = records
|
|
817
|
+
records = old.filter(record => !removed.includes(record))
|
|
818
|
+
for (const record of removed) records.push(mount(record.configs))
|
|
819
|
+
throw error
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
entries: () => records.flatMap(record => record.handlers),
|
|
823
|
+
dispose() {
|
|
824
|
+
if (!active) return
|
|
825
|
+
active = false
|
|
826
|
+
for (const record of records.splice(0).reverse()) record.dispose()
|
|
827
|
+
for (const release of listeners.splice(0).reverse()) release()
|
|
828
|
+
},
|
|
829
|
+
}
|
|
830
|
+
}
|
|
@@ -51,3 +51,9 @@ export function mountRuleSources(
|
|
|
51
51
|
sources: ReadonlyArray<RuleSourceLike>,
|
|
52
52
|
options?: MountRuleSourcesOptions,
|
|
53
53
|
): () => void
|
|
54
|
+
|
|
55
|
+
/** 同 moduleId 与 rules 引用保持未变来源;候选拒绝时恢复受影响来源的旧贡献。 */
|
|
56
|
+
export function createRuleRuntime(ctx: unknown, options?: MountRuleSourcesOptions): {
|
|
57
|
+
replace(sources: ReadonlyArray<RuleSourceLike>): void
|
|
58
|
+
dispose(): void
|
|
59
|
+
}
|
package/engine/rule-runtime.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { prepareAction } from './actions/index.mjs'
|
|
2
|
-
import {
|
|
2
|
+
import { confirmDelivered, runPreStepBatch } from './executor.mjs'
|
|
3
3
|
import { createPreStepSource, moduleSourceId } from './pre-step-source.mjs'
|
|
4
|
-
import {
|
|
4
|
+
import { createLayerRuntime } from './layers.mjs'
|
|
5
5
|
import { rebindConditions, validateDeliverySources } from './rule-spec.mjs'
|
|
6
6
|
import { wireTriggerObservers } from './trigger.mjs'
|
|
7
7
|
import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
|
|
@@ -18,42 +18,67 @@ const compare = (a, b) => a.action.channelOrder - b.action.channelOrder
|
|
|
18
18
|
|
|
19
19
|
const sourceError = (error, moduleId) => Object.assign(new Error(String(error?.message ?? error), { cause: error }), { sourceModuleId: moduleId })
|
|
20
20
|
|
|
21
|
+
const pointKey = point => `${point.channel}:${point.phase}:${point.waterfallPosition}`
|
|
22
|
+
const compareEntries = (a, b) => a.sequence - b.sequence || a.sourceModuleId.localeCompare(b.sourceModuleId)
|
|
23
|
+
|| a.ruleIndex - b.ruleIndex || a.ruleActionIndex - b.ruleActionIndex
|
|
24
|
+
|
|
25
|
+
function pointHandler(ctx, point, snapshot, warnOnce, report, isActive) {
|
|
26
|
+
return (...args) => {
|
|
27
|
+
const handlers = snapshot()
|
|
28
|
+
const free = noNext.has(point.channel)
|
|
29
|
+
const payload = free ? args : args.slice(0, -1)
|
|
30
|
+
const next = free ? () => undefined : args.at(-1)
|
|
31
|
+
if (!isActive()) return next()
|
|
32
|
+
let frame
|
|
33
|
+
try { frame = ruleFrame(point.channel, payload, warnOnce, ctx, report) }
|
|
34
|
+
catch (error) {
|
|
35
|
+
warnOnce(`rule engine: frame construction failed on ${point.channel}: ${String(error?.message ?? error)}`)
|
|
36
|
+
return next()
|
|
37
|
+
}
|
|
38
|
+
if (free) {
|
|
39
|
+
let pending
|
|
40
|
+
for (const entry of handlers) {
|
|
41
|
+
const invoke = () => {
|
|
42
|
+
if (!isActive()) return undefined
|
|
43
|
+
if (!actionMatches(entry, frame)) { entry.miss?.(frame); return undefined }
|
|
44
|
+
return entry.handler(...payload, undefined, frame)
|
|
45
|
+
}
|
|
46
|
+
pending = pending?.then ? pending.then(invoke) : invoke()
|
|
47
|
+
}
|
|
48
|
+
return pending
|
|
49
|
+
}
|
|
50
|
+
if (point.phase === 'after-next') return Promise.resolve(next()).then(async initial => {
|
|
51
|
+
let result = initial
|
|
52
|
+
for (const entry of handlers) {
|
|
53
|
+
if (!isActive()) continue
|
|
54
|
+
if (!actionMatches(entry, frame)) { entry.miss?.(frame); continue }
|
|
55
|
+
result = await entry.handler(...payload, () => result, frame)
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
})
|
|
59
|
+
const invoke = index => {
|
|
60
|
+
if (!isActive() || index === handlers.length) return next()
|
|
61
|
+
const entry = handlers[index]
|
|
62
|
+
if (!actionMatches(entry, frame)) { entry.miss?.(frame); return invoke(index + 1) }
|
|
63
|
+
return entry.handler(...payload, () => invoke(index + 1), frame)
|
|
64
|
+
}
|
|
65
|
+
return invoke(0)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
/** 所有来源只进入这个装配入口;受众限定固定效果要求 ctx 属于 options.agent 的 scope。 */
|
|
22
|
-
|
|
70
|
+
function mountSources(ctx, sources, options = {}) {
|
|
23
71
|
validateDeliverySources(sources)
|
|
24
|
-
|
|
25
|
-
// 池自己订阅 `session/event` 并统一喂事件;自建的随本挂载释放,注入的由创建者释放。
|
|
26
|
-
const coordinator = (() => {
|
|
27
|
-
const service = getService(ctx, PRE_STEP_COORDINATOR_SERVICE)
|
|
28
|
-
return service !== null && typeof service === 'object' && typeof service.registerPreset === 'function'
|
|
29
|
-
? service
|
|
30
|
-
: undefined
|
|
31
|
-
})()
|
|
32
|
-
const pool = options.promotionPool ?? coordinator?.promotionPool ?? createPromotionPool(ctx)
|
|
33
|
-
const ownsPool = options.promotionPool === undefined && coordinator?.promotionPool === undefined
|
|
72
|
+
const pool = options.promotionPool
|
|
34
73
|
// 条件绑定**在这里**发生:`compileRules` 编译期没有挂载 scope 的 ctx(preset 条件在那边恒
|
|
35
74
|
// UNAVAILABLE 且静默)。必须早于事件订阅与动作注册——动作级 `actionWhen.observe` 是订阅通道,
|
|
36
75
|
// 绑晚了那一类谓词会漏事件。`standingMountFor` 是可选兜底(官方包缺席=没有兜底,不是错误)。
|
|
37
76
|
const warnOnce = options.warnOnce ?? createWarnOnce(ctx, options.plugin ?? 'rule-engine')
|
|
38
|
-
// 非 pre-step 动作仍共享一次 ruleFrame;规则实例没有来源字段(RuleDefinition 不暴露 moduleId),
|
|
39
|
-
// 因此保留一份局部 rule→module 映射。它不是 pre-step 来源事实的重建,不参与 pre-step 的 source sink。
|
|
40
|
-
const report = options.onOutcome === undefined ? undefined : (() => {
|
|
41
|
-
const moduleOfRule = new Map()
|
|
42
|
-
for (const source of sources) for (const rule of source.rules) if (!moduleOfRule.has(rule)) moduleOfRule.set(rule, source.moduleId)
|
|
43
|
-
return (rule, channel, outcome) => options.onOutcome({ moduleId: moduleOfRule.get(rule), ruleId: rule.id, channel, outcome })
|
|
44
|
-
})()
|
|
45
77
|
const releases = []
|
|
46
|
-
// 自建池随本挂载释放;协调器/调用方注入的池由创建者释放(生命周期随归属,不跨界代管)。
|
|
47
|
-
if (ownsPool) releases.push(() => pool.dispose())
|
|
48
78
|
const points = new Map()
|
|
49
|
-
// 续跑预算按来源模块一份:同模块的 turn-stop 动作共享上限,
|
|
50
|
-
// 不再按动作各建一份。本 mount 可能含多个模块(agent-assembly 一次装载全部启用模块),
|
|
51
|
-
// 因此按 moduleId 分档;来源 id 缺失时落空串兜底键(与 wireTurnStops 同源)。
|
|
52
|
-
const turnStopBudgets = new Map()
|
|
53
79
|
const injections = new Map(sources.map(source => [source.moduleId, { moduleId: source.moduleId, sourceId: moduleSourceId(source.moduleId), configs: [], ruleActions: [] }]))
|
|
54
80
|
// 动作级分支条件(actionWhen)也要喂 session/event:否则 else / 嵌套 if 里的 phase / count
|
|
55
81
|
// 谓词永远收不到事件,状态停在冷扫那一刻(规则级 when 已由 rules 覆盖,动作级单独补齐)。
|
|
56
|
-
let active = true
|
|
57
82
|
const bind = (item, on) => prepareAction(item.action, { plugin: options.plugin, warnOnce, promptConfigOptions: item.rule.promptConfigOptions, on })(ctx)
|
|
58
83
|
try {
|
|
59
84
|
for (const source of sources) {
|
|
@@ -84,21 +109,13 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
84
109
|
let textConfigs = []
|
|
85
110
|
const flushLayers = () => {
|
|
86
111
|
if (!textConfigs.length) return
|
|
87
|
-
|
|
88
|
-
agent: options.agent,
|
|
89
|
-
turnStopBudgets,
|
|
90
|
-
sourceError,
|
|
91
|
-
on: (channel, handler) => {
|
|
92
|
-
if (channel !== point.channel) throw new TypeError(`rule action registered outside ${point.channel}: ${channel}`)
|
|
93
|
-
handlers.push({ handler }); return () => {}
|
|
94
|
-
},
|
|
95
|
-
}))
|
|
112
|
+
options.collectText(point, textConfigs)
|
|
96
113
|
textConfigs = []
|
|
97
114
|
}
|
|
98
115
|
for (const item of point.items) {
|
|
99
116
|
const { action, rule, moduleId } = item
|
|
100
117
|
if (action.kind === 'inject-text') {
|
|
101
|
-
const config = { ...action.compiledConfig, rule, sourceModuleId: moduleId, variableStore: options.variableStore, group: undefined, exclusive: false, enabled: true, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen }
|
|
118
|
+
const config = { ...action.compiledConfig, rule, sourceModuleId: moduleId, variableStore: options.variableStore, group: undefined, exclusive: false, enabled: true, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen, runtimeOrder: action.channelOrder, runtimeRuleIndex: item.ruleIndex }
|
|
102
119
|
if (config.layer === 'pre-step') injections.get(moduleId).configs.push(config)
|
|
103
120
|
else textConfigs.push(config)
|
|
104
121
|
continue
|
|
@@ -107,12 +124,11 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
107
124
|
const created = []
|
|
108
125
|
const on = (channel, handler) => {
|
|
109
126
|
if (channel !== point.channel) throw new TypeError(`rule action registered outside ${point.channel}: ${channel}`)
|
|
110
|
-
const entry = { rule, handler, sequence: action.channelOrder, sourceModuleId: moduleId, ruleId: rule.id, ruleActionIndex: action.actionIndex, id: action.id, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen }
|
|
127
|
+
const entry = { rule, handler, sequence: action.channelOrder, sourceModuleId: moduleId, ruleId: rule.id, ruleIndex: item.ruleIndex, ruleActionIndex: action.actionIndex, id: action.id, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen }
|
|
111
128
|
if (channel === 'agent/pre-step' && point.waterfallPosition === 'default') injections.get(moduleId).ruleActions.push(entry)
|
|
112
129
|
else handlers.push(entry)
|
|
113
130
|
created.push(entry)
|
|
114
|
-
//
|
|
115
|
-
// 这里再返回一个真 disposer 会与它重复释放同一批 handler。
|
|
131
|
+
// handler 由稳定执行点统一分派;来源 disposer 只释放动作自身的副作用。
|
|
116
132
|
return () => {}
|
|
117
133
|
}
|
|
118
134
|
let dispose
|
|
@@ -127,59 +143,10 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
127
143
|
}
|
|
128
144
|
flushLayers()
|
|
129
145
|
if (!handlers.length) continue
|
|
130
|
-
|
|
131
|
-
const free = noNext.has(point.channel)
|
|
132
|
-
const payload = free ? args : args.slice(0, -1)
|
|
133
|
-
const next = free ? () => undefined : args[args.length - 1]
|
|
134
|
-
if (!active) return next()
|
|
135
|
-
// 帧构造失败(如宿主 snapshotEvents 抛错)只告警并按「无事实」跳过本点动作,
|
|
136
|
-
// 不得把异常抛出整条分派。
|
|
137
|
-
let frame
|
|
138
|
-
try {
|
|
139
|
-
frame = ruleFrame(point.channel, payload, warnOnce, ctx, report)
|
|
140
|
-
} catch (error) {
|
|
141
|
-
warnOnce(`rule engine: frame construction failed on ${point.channel}: ${String(error?.message ?? error)}`)
|
|
142
|
-
return next()
|
|
143
|
-
}
|
|
144
|
-
if (free) {
|
|
145
|
-
let pending
|
|
146
|
-
for (const entry of handlers) {
|
|
147
|
-
const invoke = () => {
|
|
148
|
-
if (!active) return undefined
|
|
149
|
-
if (!actionMatches(entry, frame)) { entry.miss?.(frame); return undefined }
|
|
150
|
-
return entry.handler(...payload, undefined, frame)
|
|
151
|
-
}
|
|
152
|
-
pending = pending?.then ? pending.then(invoke) : invoke()
|
|
153
|
-
}
|
|
154
|
-
return pending
|
|
155
|
-
}
|
|
156
|
-
if (point.phase === 'after-next') return Promise.resolve(next()).then(async initial => {
|
|
157
|
-
let result = initial
|
|
158
|
-
for (const entry of handlers) {
|
|
159
|
-
if (!active) continue
|
|
160
|
-
if (!actionMatches(entry, frame)) { entry.miss?.(frame); continue }
|
|
161
|
-
result = await entry.handler(...payload, () => result, frame)
|
|
162
|
-
}
|
|
163
|
-
return result
|
|
164
|
-
})
|
|
165
|
-
const invoke = index => {
|
|
166
|
-
if (!active || index === handlers.length) return next()
|
|
167
|
-
const entry = handlers[index]
|
|
168
|
-
if (!actionMatches(entry, frame)) {
|
|
169
|
-
entry.miss?.(frame)
|
|
170
|
-
return invoke(index + 1)
|
|
171
|
-
}
|
|
172
|
-
return entry.handler(...payload, () => invoke(index + 1), frame)
|
|
173
|
-
}
|
|
174
|
-
return invoke(0)
|
|
175
|
-
}, point.waterfallPosition === 'outermost' ? { prepend: true } : undefined))
|
|
176
|
-
}
|
|
177
|
-
// 晋升相位实例对:`options.promotion` 注入(托管路径传协调器那两份),否则从池里按配置取
|
|
178
|
-
// ——池在挂载开头已建,条件绑定也用它。共用实例后事件由池统一喂,消费者一律不得再 observe。
|
|
179
|
-
const promotion = options.promotion ?? coordinator?.promotion ?? {
|
|
180
|
-
main: pool.tracker(PROMOTE_EVENTS.either, { includeSubagents: false }),
|
|
181
|
-
withSubagents: pool.tracker(PROMOTE_EVENTS.either, { includeSubagents: true }),
|
|
146
|
+
options.collectPoint(point, handlers)
|
|
182
147
|
}
|
|
148
|
+
// 各来源共用运行时拥有的晋升相位对,事件只由相位池喂入。
|
|
149
|
+
const promotion = options.promotion
|
|
183
150
|
const preStep = [...injections.values()]
|
|
184
151
|
.filter(source => source.configs.length || source.ruleActions.length)
|
|
185
152
|
.map(source => createPreStepSource({
|
|
@@ -190,14 +157,185 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
190
157
|
onOutcome: options.onOutcome === undefined ? undefined : report => options.onOutcome({ moduleId: source.moduleId, ...report }),
|
|
191
158
|
promotion,
|
|
192
159
|
}))
|
|
193
|
-
if (preStep.length) releases.push(
|
|
160
|
+
if (preStep.length) releases.push(options.registerPreStep(preStep))
|
|
194
161
|
} catch (error) {
|
|
195
|
-
active = false
|
|
196
162
|
for (const dispose of releases.reverse()) { try { dispose?.() } catch {} }
|
|
197
163
|
throw error
|
|
198
164
|
}
|
|
199
165
|
return () => {
|
|
200
|
-
active = false
|
|
201
166
|
for (const dispose of releases.splice(0).reverse()) { try { dispose?.() } catch {} }
|
|
202
167
|
}
|
|
203
168
|
}
|
|
169
|
+
|
|
170
|
+
/** 稳定执行点持有有序快照;来源仅在规则实例改变时重新绑定和注册。 */
|
|
171
|
+
export function createRuleRuntime(ctx, options = {}) {
|
|
172
|
+
const warnOnce = options.warnOnce ?? createWarnOnce(ctx, options.plugin ?? 'rule-engine')
|
|
173
|
+
const coordinatorOf = () => {
|
|
174
|
+
const value = getService(ctx, PRE_STEP_COORDINATOR_SERVICE)
|
|
175
|
+
return typeof value?.registerPreset === 'function' ? value : undefined
|
|
176
|
+
}
|
|
177
|
+
const initialCoordinator = coordinatorOf()
|
|
178
|
+
const pool = options.promotionPool ?? initialCoordinator?.promotionPool ?? createPromotionPool(ctx)
|
|
179
|
+
const ownsPool = options.promotionPool === undefined && initialCoordinator?.promotionPool === undefined
|
|
180
|
+
const promotion = options.promotion ?? initialCoordinator?.promotion ?? {
|
|
181
|
+
main: pool.tracker(PROMOTE_EVENTS.either, { includeSubagents: false }),
|
|
182
|
+
withSubagents: pool.tracker(PROMOTE_EVENTS.either, { includeSubagents: true }),
|
|
183
|
+
}
|
|
184
|
+
const standalone = new Map()
|
|
185
|
+
const registrations = new Map()
|
|
186
|
+
let registeredCoordinator = initialCoordinator
|
|
187
|
+
let active = true
|
|
188
|
+
let records = new Map()
|
|
189
|
+
let points = new Map()
|
|
190
|
+
const listeners = new Map()
|
|
191
|
+
const ruleOwners = new WeakMap()
|
|
192
|
+
const memo = new Map()
|
|
193
|
+
const report = options.onOutcome === undefined ? undefined : (rule, channel, outcome) => options.onOutcome({ moduleId: ruleOwners.get(rule), ruleId: rule.id, channel, outcome })
|
|
194
|
+
let layers
|
|
195
|
+
let releaseMemo, releaseFallback
|
|
196
|
+
const register = (coordinator, source) => {
|
|
197
|
+
if (coordinator.promotion === undefined) throw new TypeError('prompt-tool: pre-step coordinator v2 must expose a promotion owner')
|
|
198
|
+
return coordinator.registerPreset(source.sourceId, { ...source, promotion: coordinator.promotion }) ?? (() => {})
|
|
199
|
+
}
|
|
200
|
+
const ensureFallback = () => {
|
|
201
|
+
if (releaseFallback !== undefined) return
|
|
202
|
+
releaseMemo = ctx.on('session/event', (session, event) => confirmDelivered(memo, session, event))
|
|
203
|
+
try {
|
|
204
|
+
releaseFallback = ctx.on('agent/pre-step', async (payload, next) => {
|
|
205
|
+
// 取得本次完整来源快照后才进入宿主,途中更新不会拼接两个版本的批次。
|
|
206
|
+
const coordinator = coordinatorOf()
|
|
207
|
+
if (coordinator !== registeredCoordinator) {
|
|
208
|
+
for (const release of registrations.values()) release()
|
|
209
|
+
registrations.clear()
|
|
210
|
+
registeredCoordinator = coordinator
|
|
211
|
+
if (coordinator !== undefined) for (const source of standalone.values()) registrations.set(source.sourceId, register(coordinator, source))
|
|
212
|
+
}
|
|
213
|
+
const sources = [...standalone.values()]
|
|
214
|
+
const decision = await next()
|
|
215
|
+
if (!active || coordinator !== undefined || sources.length === 0) return decision
|
|
216
|
+
return runPreStepBatch({ sources, ctx, payload, agent: payload?.agent, decision, memo, warnOnce })
|
|
217
|
+
}, { prepend: true })
|
|
218
|
+
} catch (error) { releaseMemo(); releaseMemo = undefined; throw error }
|
|
219
|
+
}
|
|
220
|
+
const mount = source => {
|
|
221
|
+
const record = { source, points: [], text: [], dispose: undefined }
|
|
222
|
+
for (const rule of source.rules) ruleOwners.set(rule, source.moduleId)
|
|
223
|
+
record.dispose = mountSources(ctx, [source], {
|
|
224
|
+
...options, warnOnce, promotionPool: pool, promotion,
|
|
225
|
+
registerPreStep(sources) {
|
|
226
|
+
const installed = []
|
|
227
|
+
const dispose = () => {
|
|
228
|
+
for (const item of installed) if (standalone.get(item.sourceId) === item) {
|
|
229
|
+
standalone.delete(item.sourceId)
|
|
230
|
+
registrations.get(item.sourceId)?.()
|
|
231
|
+
registrations.delete(item.sourceId)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
ensureFallback()
|
|
236
|
+
for (const item of sources) {
|
|
237
|
+
if (standalone.has(item.sourceId)) throw new TypeError(`prompt-tool: duplicate pre-step source ${item.sourceId}`)
|
|
238
|
+
standalone.set(item.sourceId, item)
|
|
239
|
+
installed.push(item)
|
|
240
|
+
const coordinator = coordinatorOf()
|
|
241
|
+
if (coordinator !== undefined) {
|
|
242
|
+
try { registrations.set(item.sourceId, register(coordinator, item)) }
|
|
243
|
+
catch (error) { throw sourceError(error, source.moduleId) }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return dispose
|
|
247
|
+
} catch (error) { dispose(); throw error }
|
|
248
|
+
},
|
|
249
|
+
collectPoint(point, handlers) { record.points.push({ point, handlers }) },
|
|
250
|
+
collectText(point, configs) {
|
|
251
|
+
for (const config of configs) { config.runtimePoint = point; record.text.push(config) }
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
return record
|
|
255
|
+
}
|
|
256
|
+
const publishPoints = () => {
|
|
257
|
+
const next = new Map()
|
|
258
|
+
const add = (point, entry) => {
|
|
259
|
+
const key = pointKey(point)
|
|
260
|
+
const value = next.get(key) ?? { point, handlers: [] }
|
|
261
|
+
value.handlers.push(entry)
|
|
262
|
+
next.set(key, value)
|
|
263
|
+
}
|
|
264
|
+
for (const record of records.values()) for (const { point, handlers } of record.points) for (const entry of handlers) add(point, entry)
|
|
265
|
+
for (const { channel, handler, config } of layers?.entries() ?? []) {
|
|
266
|
+
const point = { ...config.runtimePoint, channel }
|
|
267
|
+
add(point, { handler, sequence: config.runtimeOrder, sourceModuleId: config.sourceModuleId, ruleIndex: config.runtimeRuleIndex, ruleActionIndex: config.ruleActionIndex ?? 0 })
|
|
268
|
+
}
|
|
269
|
+
for (const [key, value] of next) {
|
|
270
|
+
value.handlers.sort(compareEntries)
|
|
271
|
+
if (!listeners.has(key)) listeners.set(key, ctx.on(value.point.channel,
|
|
272
|
+
pointHandler(ctx, value.point, () => points.get(key)?.handlers ?? [], warnOnce, report, () => active),
|
|
273
|
+
value.point.waterfallPosition === 'outermost' ? { prepend: true } : undefined))
|
|
274
|
+
}
|
|
275
|
+
points = next
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
replace(sources) {
|
|
279
|
+
if (!active) throw new Error('rule runtime disposed')
|
|
280
|
+
validateDeliverySources(sources)
|
|
281
|
+
const desired = new Map()
|
|
282
|
+
for (const source of sources) {
|
|
283
|
+
if (desired.has(source.moduleId)) throw sourceError(new TypeError(`duplicate rule source ${source.moduleId}`), source.moduleId)
|
|
284
|
+
desired.set(source.moduleId, source)
|
|
285
|
+
}
|
|
286
|
+
const old = records
|
|
287
|
+
const changed = [...old.values()].filter(record => desired.get(record.source.moduleId)?.rules !== record.source.rules)
|
|
288
|
+
if (changed.length === 0 && sources.length === old.size) return
|
|
289
|
+
for (const record of changed) record.dispose()
|
|
290
|
+
const created = []
|
|
291
|
+
try {
|
|
292
|
+
const next = new Map()
|
|
293
|
+
for (const source of sources) {
|
|
294
|
+
let record = old.get(source.moduleId)
|
|
295
|
+
if (record?.source.rules !== source.rules) {
|
|
296
|
+
record = mount(source)
|
|
297
|
+
created.push(record)
|
|
298
|
+
}
|
|
299
|
+
next.set(source.moduleId, record)
|
|
300
|
+
}
|
|
301
|
+
records = next
|
|
302
|
+
const text = [...records.values()].flatMap(record => record.text)
|
|
303
|
+
if (text.length > 0) layers ??= createLayerRuntime(ctx, warnOnce, { agent: options.agent, turnStopBudgets: new Map(), sourceError })
|
|
304
|
+
layers?.replace(text)
|
|
305
|
+
publishPoints()
|
|
306
|
+
} catch (error) {
|
|
307
|
+
for (const record of created.reverse()) record.dispose()
|
|
308
|
+
records = new Map(old)
|
|
309
|
+
try {
|
|
310
|
+
for (const record of changed) records.set(record.source.moduleId, mount(record.source))
|
|
311
|
+
layers?.replace([...records.values()].flatMap(record => record.text))
|
|
312
|
+
publishPoints()
|
|
313
|
+
} catch (restoreError) {
|
|
314
|
+
throw Object.assign(new AggregateError([error, restoreError], `rule source replacement and recovery failed: ${String(error.message)}; ${String(restoreError.message)}`), { sourceModuleId: error.sourceModuleId })
|
|
315
|
+
}
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
dispose() {
|
|
320
|
+
if (!active) return
|
|
321
|
+
active = false
|
|
322
|
+
for (const record of records.values()) record.dispose()
|
|
323
|
+
records.clear()
|
|
324
|
+
layers?.dispose()
|
|
325
|
+
for (const release of listeners.values()) release()
|
|
326
|
+
listeners.clear()
|
|
327
|
+
points.clear()
|
|
328
|
+
releaseFallback?.()
|
|
329
|
+
releaseMemo?.()
|
|
330
|
+
if (ownsPool) pool.dispose()
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 兼容一次性挂载;增量调用者持有 createRuleRuntime 的来源集合。 */
|
|
336
|
+
export function mountRuleSources(ctx, sources, options = {}) {
|
|
337
|
+
validateDeliverySources(sources)
|
|
338
|
+
const runtime = createRuleRuntime(ctx, options)
|
|
339
|
+
try { runtime.replace(sources) } catch (error) { runtime.dispose(); throw error }
|
|
340
|
+
return () => runtime.dispose()
|
|
341
|
+
}
|