dsh-plugin-prompt-tool 2.1.0 → 3.0.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 +100 -233
- package/engine/actions/append-context.mjs +3 -24
- package/engine/actions/assembly.mjs +70 -94
- package/engine/actions/catalog.mjs +65 -28
- package/engine/actions/content.mjs +8 -152
- package/engine/actions/examples.mjs +8 -4
- package/engine/actions/inbox-prepend.mjs +4 -5
- package/engine/actions/index.mjs +17 -9
- package/engine/actions/inject-text.mjs +24 -4
- package/engine/actions/pre-step-filter.mjs +3 -4
- package/engine/actions/registration.mjs +20 -28
- package/engine/actions/request-params.mjs +21 -1
- package/engine/actions/shared.mjs +22 -13
- package/engine/actions/stream-output.mjs +26 -0
- package/engine/actions/tool-gate.mjs +31 -0
- package/engine/actions/tool-result.mjs +36 -0
- package/engine/actions/tool-visibility.mjs +248 -0
- package/engine/agent-capability.mjs +67 -0
- package/engine/anchor-match.mjs +14 -21
- package/engine/audience.d.mts +2 -0
- package/engine/audience.mjs +102 -0
- package/engine/branch.d.mts +4 -0
- package/engine/branch.mjs +61 -18
- package/engine/compaction-epoch.d.mts +25 -0
- package/engine/compaction-epoch.mjs +27 -4
- package/engine/compositions/source/local/filesystem-editor.yml +3 -3
- package/engine/compositions/source/local/instruction-hint.yml +2 -4
- package/engine/compositions/source/local/module-file-tools.yml +2 -0
- package/engine/compositions/source/local/module-import-tools.yml +4 -0
- package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
- package/engine/compositions/source/local/module-rule-tools.yml +5 -0
- package/engine/compositions/source/local/module-variable-tools.yml +5 -0
- package/engine/compositions/source/local/present-as.yml +26 -0
- package/engine/compositions/source/local/tool-config-engine.yml +2 -3
- package/engine/compositions/source/local/tool-search.yml +11 -0
- package/engine/condition-facts.mjs +84 -0
- package/engine/conditions/anchor.mjs +63 -23
- package/engine/conditions/availability.mjs +1 -1
- package/engine/conditions/composite.mjs +3 -3
- package/engine/conditions/count.mjs +42 -11
- package/engine/conditions/evaluation.mjs +74 -19
- package/engine/conditions/examples.mjs +2 -1
- package/engine/conditions/index.d.mts +15 -0
- package/engine/conditions/index.mjs +23 -9
- package/engine/conditions/operators.mjs +4 -0
- package/engine/conditions/phase.mjs +9 -3
- package/engine/conditions/preset.mjs +1 -1
- package/engine/conditions/session.mjs +8 -2
- package/engine/conditions/subject.mjs +144 -65
- package/engine/conditions/text.mjs +26 -8
- package/engine/conditions/variable.mjs +83 -0
- package/engine/context-activation.d.mts +11 -0
- package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
- package/engine/delivery.d.mts +2 -0
- package/engine/delivery.mjs +93 -0
- package/engine/discovery.d.mts +38 -0
- package/engine/discovery.mjs +146 -0
- package/engine/env-safety.mjs +14 -0
- package/engine/executor.d.mts +28 -0
- package/engine/executor.mjs +290 -205
- package/engine/fillers.mjs +39 -8
- package/engine/fixed-registration.mjs +12 -0
- package/engine/history.mjs +71 -0
- package/engine/instruction-blocks.d.mts +13 -0
- package/engine/instruction-blocks.mjs +21 -0
- package/engine/instruction-files.d.mts +5 -0
- package/engine/instruction-files.mjs +97 -0
- package/engine/instruction-hint.mjs +69 -103
- package/engine/interpolate.d.mts +7 -0
- package/engine/interpolate.mjs +44 -13
- package/engine/layers.mjs +163 -227
- package/engine/machine-paths.mjs +40 -0
- package/engine/module-file-tools.mjs +8 -0
- package/engine/module-import-tools.mjs +10 -0
- package/engine/module-manifest-tools.mjs +8 -0
- package/engine/module-rule-tools.mjs +10 -0
- package/engine/module-variable-tools.mjs +10 -0
- package/engine/order.d.mts +14 -0
- package/engine/order.mjs +0 -3
- package/engine/path-safety.d.mts +2 -0
- package/engine/path-safety.mjs +58 -0
- package/engine/pre-step-source.d.mts +69 -0
- package/engine/pre-step-source.mjs +103 -0
- package/engine/present-as.mjs +77 -0
- package/engine/promotion-pool.d.mts +9 -0
- package/engine/promotion-pool.mjs +69 -0
- package/engine/record.mjs +2 -0
- package/engine/rule-engine.mjs +3 -2
- package/engine/rule-facts.d.mts +11 -0
- package/engine/rule-facts.mjs +60 -0
- package/engine/rule-runtime.d.mts +46 -0
- package/engine/rule-runtime.mjs +98 -13
- package/engine/rule-spec.d.mts +106 -0
- package/engine/rule-spec.mjs +301 -27
- package/engine/run-code-env.mjs +73 -19
- package/engine/schema.mjs +165 -243
- package/engine/sdk-strip.mjs +2 -2
- package/engine/service-names.d.mts +10 -0
- package/engine/service-names.mjs +10 -0
- package/engine/shared.d.mts +21 -0
- package/engine/shared.mjs +46 -13
- package/engine/skill-search.mjs +125 -53
- package/engine/source-kind.d.mts +9 -0
- package/engine/source-kind.mjs +16 -0
- package/engine/strategy-ids.mjs +5 -0
- package/engine/template-evaluation.d.mts +16 -0
- package/engine/template-evaluation.mjs +178 -0
- package/engine/template-frame.d.mts +16 -0
- package/engine/template-frame.mjs +69 -0
- package/engine/tool-config-engine.mjs +38 -34
- package/engine/tool-definition.d.mts +4 -0
- package/engine/tool-definition.mjs +5 -1
- package/engine/tool-git-bash.mjs +5 -1
- package/engine/tool-scope.mjs +22 -0
- package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
- package/engine/trigger.mjs +15 -270
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
- package/engine/vendor/yaml/package.json +1 -1
- package/lib/client.js +8312 -6724
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2120
- package/lib/index.mjs +26914 -5816
- package/modules/ponytail/module.yml +83 -203
- package/modules/ptc-mode/module.yml +45 -0
- package/modules/soul/assets/core-hard.md +6 -0
- package/modules/soul/assets/criteria.md +12 -0
- package/modules/soul/assets/echo.md +12 -0
- package/modules/soul/assets/layers.md +11 -0
- package/modules/soul/assets/ledger.md +9 -0
- package/modules/soul/assets/manifest.yml +20 -0
- package/modules/soul/assets/reference/00-layers.md +27 -0
- package/modules/soul/assets/reference/01-criteria.md +32 -0
- package/modules/soul/assets/reference/02-echo.md +158 -0
- package/modules/soul/assets/reference/03-vital.md +183 -0
- package/modules/soul/assets/reference/04-manifest.json +106 -0
- package/modules/soul/assets/reference/05-doctor.md +67 -0
- package/modules/soul/assets/reference/06-reference.md +189 -0
- package/modules/soul/assets/reference/07-bundle.json +431 -0
- package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
- package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
- package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
- package/modules/soul/assets/reference/11-self.md +80 -0
- package/modules/soul/assets/reference/12-ledger.md +159 -0
- package/modules/soul/assets/reference/README.md +10 -0
- package/modules/soul/assets/templates/agents-template.md +6 -0
- package/modules/soul/assets/vital.md +9 -0
- package/modules/soul/module.yml +311 -0
- package/modules/superpowers/module.yml +122 -0
- package/modules/tool-surface/module.yml +51 -47
- package/package.json +37 -40
- package/skills/dsh-module/SKILL.md +67 -147
- package/skills/dsh-module/reference.md +113 -86
- package/templates/10-pre-step.yml +2 -3
- package/templates/14-first-turn-anchor.yml +10 -14
- package/templates/15-guide-auto.yml +32 -28
- package/templates/16-custom-fallback.yml +33 -21
- package/templates/20-system-section.yml +1 -4
- package/templates/50-llm-stream.yml +4 -10
- package/templates/60-tool-pipeline.yml +12 -10
- package/templates/63-append-context.yml +4 -6
- package/templates/65-turn-stop.yml +2 -2
- package/templates/67-inbox-prepend.yml +2 -2
- package/templates/70-subagent-maintenance.yml +9 -10
- package/templates/75-subagent-end.yml +2 -2
- package/templates/80-tool-surface.yml +41 -57
- package/templates/tools/40-delegate-module-import.yml +22 -0
- package/templates/tools/50-delegate-module-rules.yml +14 -0
- package/templates/tools/60-delegate-module-variables.yml +17 -0
- package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
- package/engine/actions/decision.mjs +0 -54
- package/engine/actions/guard.mjs +0 -80
- package/engine/actions/sdk-strip.mjs +0 -30
- package/engine/character-tools.mjs +0 -8
- package/engine/classify-task.mjs +0 -53
- package/engine/compositions/source/local/character-tools.yml +0 -5
- package/engine/compositions/source/local/dev-tool-search.yml +0 -11
- package/engine/compositions/source/local/session-var-tools.yml +0 -5
- package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
- package/engine/compositions/source/local/world-book-tools.yml +0 -5
- package/engine/condition.mjs +0 -96
- package/engine/declared-triggers.mjs +0 -93
- package/engine/session-var-tools.mjs +0 -8
- package/engine/session-vars.mjs +0 -54
- package/engine/st-macros.d.mts +0 -22
- package/engine/st-macros.mjs +0 -208
- package/engine/st-render.mjs +0 -109
- package/engine/st-world-book.d.mts +0 -10
- package/engine/subagent-tool-policy-core.mjs +0 -400
- package/engine/subagent-tool-policy.mjs +0 -352
- package/engine/trigger-spec.mjs +0 -173
- package/engine/world-book-tools.mjs +0 -8
- package/lib/migrate-rules.d.mts +0 -1
- package/lib/migrate-rules.mjs +0 -610
- package/lib/module-package-DqJZaE71.mjs +0 -2
- package/lib/module-package-q2gDRu9H.mjs +0 -2869
- package/lib/rule-builder-DMrJten6.mjs +0 -14767
- package/templates/61-deny-tools-presentation.yml +0 -27
- package/templates/62-guard-deny-tools.yml +0 -20
- package/templates/policies/legacy-defaults.yml +0 -34
- package/templates/tools/40-delegate-character.yml +0 -15
- package/templates/tools/50-delegate-worldbook.yml +0 -15
- package/templates/tools/60-delegate-session-var.yml +0 -15
- package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
package/engine/rule-spec.mjs
CHANGED
|
@@ -1,17 +1,136 @@
|
|
|
1
|
-
/** module.yml.rules
|
|
1
|
+
/** module.yml.rules 的唯一运行时编译入口。 */
|
|
2
2
|
import { ACTION_KINDS, actionExecutionPoint, prepareAction, validateActionOptions } from './actions/index.mjs'
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
// 字段清单与动作声明同住 `actions/catalog.mjs`(`actions/index.mjs` 不再导出新面)。
|
|
4
|
+
import { ACTION_FIELDS } from './actions/catalog.mjs'
|
|
5
|
+
import { compileWhen, PREDICATE_FACTORIES, COMPOSITE_OPERATORS, FACT_PREDICATE_SUBJECTS, channelFactSubjects, channelTextSubjects } from './conditions/index.mjs'
|
|
6
|
+
import { createPromptConfigs, INJECT_CONFIG_FIELDS, KNOWN_LAYERS, SESSION_VARIABLES_DISABLED, assertLlmCallPatch } from './schema.mjs'
|
|
7
|
+
import { stripDeclaredRefs } from './interpolate.mjs'
|
|
8
|
+
import { matchesAudience, validateConfig } from './shared.mjs'
|
|
5
9
|
import { WATERFALL_POSITIONS } from './trigger.mjs'
|
|
6
10
|
import { ACTION_EXAMPLES } from './actions/examples.mjs'
|
|
7
11
|
import { PREDICATE_EXAMPLES } from './conditions/examples.mjs'
|
|
8
12
|
import { conjunction, expandActions } from './branch.mjs'
|
|
13
|
+
import { identityOf, textRegistrationName } from './delivery.mjs'
|
|
14
|
+
import { AUDIENCE_ALL, audienceOf, intersectAudiences } from './audience.mjs'
|
|
15
|
+
import { isFixedRegistration } from './fixed-registration.mjs'
|
|
16
|
+
import { record } from './record.mjs'
|
|
9
17
|
|
|
10
18
|
/** 规则字段:只有 `if`/`then`/`else`;旧名 `when`/`do` 留在名单里,只为把 unknown fields 换成一句「已退役,改用 X」。 */
|
|
11
19
|
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
20
|
const nonempty = value => typeof value === 'string' && value.trim().length > 0
|
|
14
21
|
|
|
22
|
+
/** `prepend` 退役文案(动作层与 `inject-text.config` 共用):等价档只有「注册到最外层」一档,而 `inject-text` 连它也不接受。 */
|
|
23
|
+
const PREPEND_RETIRED = '`prepend` 已取消 — 它是未文档化的注册后门(executor 直读 config.prepend),作用只有「注册到最外层」一档:非 inject-text 动作改用 `waterfallPosition: outermost`;`inject-text` 不接受该字段,没有等价写法'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 条件树引用的全部「谓词 → 通道事实」对(含组合与否定);`subject === undefined` = 作者漏写。
|
|
27
|
+
* 树是数据:校验期读它,挂载期由 `rebindConditions` 用它重绑——`compileWhen` 的产物把 subject 关进闭包,读不回来。
|
|
28
|
+
* `text` 的 subject 由作者声明;事实谓词读的事实取自谓词侧的 `FACT_PREDICATE_SUBJECTS`
|
|
29
|
+
* (唯一登记处,见 `conditions/subject.mjs`),本函数不再逐谓词手抄。
|
|
30
|
+
*/
|
|
31
|
+
function conditionSubjects(node, out = []) {
|
|
32
|
+
if (!record(node)) return out
|
|
33
|
+
for (const [key, value] of Object.entries(node)) {
|
|
34
|
+
if (key === 'text' && record(value)) out.push({ predicate: 'text', subject: value.subject })
|
|
35
|
+
else if (Object.hasOwn(FACT_PREDICATE_SUBJECTS, key)) out.push({ predicate: key, subject: FACT_PREDICATE_SUBJECTS[key] })
|
|
36
|
+
else if (key === 'not') conditionSubjects(value, out)
|
|
37
|
+
else if ((key === 'all' || key === 'any' || key === 'notAny') && Array.isArray(value)) {
|
|
38
|
+
for (const child of value) conditionSubjects(child, out)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function referencedActions(node, out = new Set()) {
|
|
45
|
+
if (!record(node)) return out
|
|
46
|
+
if (node.anchor?.after) out.add(node.anchor.after.action)
|
|
47
|
+
for (const key of ['all', 'any', 'notAny']) {
|
|
48
|
+
if (Array.isArray(node[key])) for (const child of node[key]) referencedActions(child, out)
|
|
49
|
+
}
|
|
50
|
+
if (node.not) referencedActions(node.not, out)
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateActionLinks(rule) {
|
|
55
|
+
const actions = new Map(rule.actions.map(action => [action.id, action]))
|
|
56
|
+
const edges = new Map()
|
|
57
|
+
for (const action of rule.actions) {
|
|
58
|
+
const content = action.compiledConfig
|
|
59
|
+
if (content?.fill === 'tool-catalog') {
|
|
60
|
+
const source = actions.get(content.params.action)
|
|
61
|
+
if (source?.kind !== 'tool-visibility' || !Array.isArray(source.allow)) {
|
|
62
|
+
throw new TypeError(`rule ${rule.id}: tool-catalog action ${action.id} requires a same-rule tool-visibility action with allow`)
|
|
63
|
+
}
|
|
64
|
+
content.toolCatalogSource = { allow: source.allow, deny: source.deny, discovery: source.discovery }
|
|
65
|
+
}
|
|
66
|
+
const refs = referencedActions(action.conditions, referencedActions(rule.conditionTrees.when))
|
|
67
|
+
edges.set(action.id, refs)
|
|
68
|
+
if (refs.size && action.compiledConfig) action.compiledConfig.linkedDelivery = true
|
|
69
|
+
for (const id of refs) {
|
|
70
|
+
const target = actions.get(id)
|
|
71
|
+
if (target === undefined) throw new TypeError(`rule ${rule.id}: unknown after action ${id}`)
|
|
72
|
+
if (id === action.id) throw new TypeError(`rule ${rule.id}: action ${id} cannot reference itself in after`)
|
|
73
|
+
const config = target.compiledConfig
|
|
74
|
+
if (target.kind !== 'inject-text' || !['pre-step', 'runtime-context', 'turn-stop'].includes(config?.layer)
|
|
75
|
+
|| config.mergeMode === 'merged') throw new TypeError(`rule ${rule.id}: after action ${id} requires an independently delivered message or runtime-context`)
|
|
76
|
+
for (const other of rule.actions) {
|
|
77
|
+
if (other === target || other.compiledConfig === undefined) continue
|
|
78
|
+
const conflict = config.layer === 'runtime-context'
|
|
79
|
+
? other.compiledConfig.layer === config.layer && textRegistrationName(config, 'contextName') === textRegistrationName(other.compiledConfig, 'contextName')
|
|
80
|
+
: identityOf(config) === identityOf(other.compiledConfig)
|
|
81
|
+
if (conflict) throw new TypeError(`rule ${rule.id}: after action ${id} has an ambiguous delivery identity`)
|
|
82
|
+
}
|
|
83
|
+
config.linkedDelivery = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const visiting = new Set(), visited = new Set()
|
|
87
|
+
const visit = id => {
|
|
88
|
+
if (visiting.has(id)) throw new TypeError(`rule ${rule.id}: cyclic after action reference at ${id}`)
|
|
89
|
+
if (visited.has(id)) return
|
|
90
|
+
visiting.add(id)
|
|
91
|
+
for (const target of edges.get(id) ?? []) visit(target)
|
|
92
|
+
visiting.delete(id); visited.add(id)
|
|
93
|
+
}
|
|
94
|
+
for (const id of actions.keys()) visit(id)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 命名快照在整个挂载 scope 内共享,关联目标不能借别的规则的同名贡献确认。 */
|
|
98
|
+
export function validateDeliverySources(sources) {
|
|
99
|
+
const names = new Map()
|
|
100
|
+
for (const source of sources) for (const rule of source.rules) {
|
|
101
|
+
if (rule.enabled === false) continue
|
|
102
|
+
for (const action of rule.actions) {
|
|
103
|
+
if (action.compiledConfig?.layer !== 'runtime-context') continue
|
|
104
|
+
const config = { ...action.compiledConfig, sourceModuleId: source.moduleId }
|
|
105
|
+
const name = textRegistrationName(config, 'contextName')
|
|
106
|
+
const previous = names.get(name)
|
|
107
|
+
if (previous && (previous.linkedDelivery || config.linkedDelivery)) {
|
|
108
|
+
throw new TypeError(`ambiguous runtime-context delivery identity ${JSON.stringify(name)}`)
|
|
109
|
+
}
|
|
110
|
+
names.set(name, config)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* **只用于动作级条件**:动作的判定绑定单一执行点,谓词要读的事实写错通道(或 `text` 省略
|
|
117
|
+
* subject)等于该动作永不执行,而挂载与运行期都不报错(`availability.mjs` 对通道取不到的
|
|
118
|
+
* 字段返回 UNAVAILABLE,`condition-facts.mjs#subjectTextOf` 取不到一律空串)。规则级 `if` 相反:
|
|
119
|
+
* 它在该规则每个动作的执行点上各自求值,「缺事实即不执行」是三值语义的设计意图(见
|
|
120
|
+
* `test/engine/rules.test.mjs` 的缺事实用例),因此编译期不拒绝。
|
|
121
|
+
*/
|
|
122
|
+
function assertSubject(predicate, subject, allowed, label) {
|
|
123
|
+
if (subject === undefined) {
|
|
124
|
+
throw new TypeError(`${label}: ${predicate} needs an explicit subject — 省略时判定恒为「缺事实」,规则永不命中`)
|
|
125
|
+
}
|
|
126
|
+
if (!allowed.includes(subject)) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
`${label}: ${predicate} subject ${JSON.stringify(subject)} is unavailable at this execution point`
|
|
129
|
+
+ ` — 本执行点可用 ${allowed.length > 0 ? allowed.join(', ') : '(无)'};写死条件不会命中`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
15
134
|
/** 规则及物化身份的唯一安全边界;host 保存、导入与直接编译共用。 */
|
|
16
135
|
export function assertRuleId(id) {
|
|
17
136
|
if (!nonempty(id)) throw new TypeError('rule id must be a non-empty string')
|
|
@@ -24,11 +143,13 @@ export function assertRuleId(id) {
|
|
|
24
143
|
/** 注入叶子的唯一身份投影;稳定动作 id 保证重排不改变投递身份。 */
|
|
25
144
|
export function injectionConfigSpec(rule, action, options = {}) {
|
|
26
145
|
if (action?.kind !== 'inject-text' || !record(action.config)) throw new TypeError('inject-text config must be an object')
|
|
146
|
+
if (Object.hasOwn(action.config, 'variables')) throw new TypeError('动作 config.variables 已退役;请显式提升为模块 variables,同名不同值须先解决冲突')
|
|
27
147
|
const config = { ...action.config, id: action.config.id ?? `rule:${encodeURIComponent(rule.id)}:${encodeURIComponent(action.id)}`, layer: action.config.layer ?? rule.layer ?? 'pre-step' }
|
|
28
148
|
const variables = record(options.variables) ? options.variables : {}
|
|
29
149
|
if (options.variablesEnabled === false) {
|
|
30
150
|
const keys = new Set(Object.keys(variables))
|
|
31
|
-
|
|
151
|
+
// 剥离复用插值语法(同一条 REFERENCE_RE):花括号内空白与 `::参数` 形态也算引用。
|
|
152
|
+
const strip = text => stripDeclaredRefs(text, keys)
|
|
32
153
|
if (config.text !== undefined) config.text = strip(config.text)
|
|
33
154
|
if (Array.isArray(config.texts)) config.texts = config.texts.map(strip)
|
|
34
155
|
if (record(config.params) && typeof config.params.text === 'string') config.params = { ...config.params, text: strip(config.params.text) }
|
|
@@ -50,9 +171,36 @@ export function validateRuleGroups(rules) {
|
|
|
50
171
|
}
|
|
51
172
|
}
|
|
52
173
|
|
|
174
|
+
/** 编译与宿主聚合共用的受众独占清单,身份保留到规则和动作。 */
|
|
175
|
+
export function completeRuleSections(rules, audience) {
|
|
176
|
+
return rules.filter(rule => rule.enabled !== false).flatMap(rule => rule.actions.flatMap(action => {
|
|
177
|
+
const config = action.compiledConfig ?? action.config
|
|
178
|
+
return action.kind === 'inject-text' && config?.layer === 'system-section' && config.params?.complete === true
|
|
179
|
+
&& matchesAudience(config.audience, audience) ? [`${rule.id}/${action.id}`] : []
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 动作级受众事实:规则级 `when` 与动作级 `conditions` 的合取。
|
|
185
|
+
*
|
|
186
|
+
* 规则级 `else` 的动作自带 `not(if)` 并标记 `bypassRuleWhen`——它的受众已由动作条件
|
|
187
|
+
* 决定,再叠 `when` 会得到 `when ∧ ¬when` 的空集。未知 `actionId` 是防御分支:
|
|
188
|
+
* 调用方只在遍历真实动作时使用,遇到未知 id 按两个受众处理。
|
|
189
|
+
*/
|
|
190
|
+
export function actionAudiences(rule, actionId) {
|
|
191
|
+
const actions = Array.isArray(rule?.actions) ? rule.actions : []
|
|
192
|
+
const action = actions.find((item) => record(item) && item.id === actionId)
|
|
193
|
+
if (action === undefined) return new Set(AUDIENCE_ALL)
|
|
194
|
+
const own = audienceOf(action.conditions)
|
|
195
|
+
const when = action.bypassRuleWhen === true ? undefined : audienceOf(rule?.conditionTrees?.when)
|
|
196
|
+
if (when === undefined && own === undefined) return new Set(AUDIENCE_ALL)
|
|
197
|
+
if (when === undefined) return new Set(own)
|
|
198
|
+
if (own === undefined) return new Set(when)
|
|
199
|
+
return intersectAudiences(when, own)
|
|
200
|
+
}
|
|
201
|
+
|
|
53
202
|
export function ruleActionExecution(action) {
|
|
54
|
-
const fixed = action.kind === '
|
|
55
|
-
&& (action.config?.params?.complete === true || action.config?.params?.suppressRuntimeContext === true))
|
|
203
|
+
const fixed = action.kind === 'inject-text' && isFixedRegistration(action.config)
|
|
56
204
|
return { ...actionExecutionPoint(action), lifecycle: fixed ? 'registration' : 'event' }
|
|
57
205
|
}
|
|
58
206
|
|
|
@@ -62,10 +210,13 @@ function normalizeActionGates(action, execution) {
|
|
|
62
210
|
if (!inject && action.kind !== 'request-params') return
|
|
63
211
|
const target = inject ? action.config : action
|
|
64
212
|
if (inject) {
|
|
65
|
-
|
|
66
|
-
|
|
213
|
+
// 这三个键在 `rule-runtime.mjs` 组装注入配置时被逐条覆盖(group/exclusive/enabled),
|
|
214
|
+
// 动作不另建启停和互斥状态,避免出现第二个规则总闸。
|
|
215
|
+
for (const field of ['enabled', 'group', 'exclusive']) {
|
|
216
|
+
if (target[field] !== undefined) throw new TypeError(`action ${action.id}: config.${field} 属于规则层,移到 rule.${field}`)
|
|
217
|
+
}
|
|
67
218
|
}
|
|
68
|
-
const fields = inject ? ['audience', 'modelScope', 'promotion'
|
|
219
|
+
const fields = inject ? ['audience', 'modelScope', 'promotion'] : ['audience', 'modelScope']
|
|
69
220
|
for (const field of fields) {
|
|
70
221
|
const value = target[field]
|
|
71
222
|
if (inject && field === 'audience' && target.layer === 'system-section' && execution.lifecycle === 'registration' && ['main', 'subagent'].includes(value)) continue
|
|
@@ -75,12 +226,13 @@ function normalizeActionGates(action, execution) {
|
|
|
75
226
|
}
|
|
76
227
|
}
|
|
77
228
|
|
|
78
|
-
/** 注册制层:内容在 agent 装配时注册一次,没有逐轮求值时机,因此不接受分支条件。 */
|
|
79
|
-
const REGISTRATION_LAYERS = new Set(['system-section', 'runtime-context'])
|
|
80
|
-
|
|
81
229
|
/** options.promptConfigOptions 拥有模板/策略路径;configOrder 使用规则 id,moduleId 只标记来源。 */
|
|
82
230
|
export function compileRules(specs, options = {}) {
|
|
83
231
|
if (!Array.isArray(specs)) throw new TypeError('rules must be an array')
|
|
232
|
+
// 条件的绑定**不在编译期**:`preset` 谓词的取法要挂载 scope 的 ctx(`conditions/preset.mjs`),
|
|
233
|
+
// 准备期拿不到。这里显式剥掉挂载期选项——传了也不生效,免得调用方以为已经绑好。
|
|
234
|
+
// variableStore 与 ctx/standingMountFor 同理:绑定发生在挂载期,准备期传了也不生效。
|
|
235
|
+
const { ctx: _ctx, standingMountFor: _standingMountFor, variableStore: _variableStore, ...conditionOptions } = options
|
|
84
236
|
const ids = new Set()
|
|
85
237
|
const pendingConfigs = []
|
|
86
238
|
const moduleId = options.moduleId ?? options.sourceModuleId
|
|
@@ -98,17 +250,25 @@ export function compileRules(specs, options = {}) {
|
|
|
98
250
|
if (spec.when !== undefined) throw new TypeError(`rule ${spec.id}: "when" 已退役,改用 "if"`)
|
|
99
251
|
if (spec.do !== undefined) throw new TypeError(`rule ${spec.id}: "do" 已退役,改用 "then"`)
|
|
100
252
|
const ruleIf = spec.if
|
|
253
|
+
// 规则级 `if` 跨执行点各自求值,不在这里判定(见 assertSubject 的说明)。`else` 的动作自带
|
|
254
|
+
// 这个 `not(if)`,校验时按**对象身份**剔除(`branch.mjs#expandActions` 保留 `outer` 元素
|
|
255
|
+
// 引用)——按 subject 名字跳过会连带跳过嵌套分支里同名、但来自另一个执行点的死条件。
|
|
256
|
+
const ruleNegation = ruleIf === undefined ? undefined : { not: ruleIf }
|
|
101
257
|
const ruleThen = spec.then
|
|
102
258
|
if (!Array.isArray(ruleThen) || ruleThen.length === 0) throw new TypeError(`rule ${spec.id}.then must be a non-empty array`)
|
|
103
259
|
const sequence = options.configOrder?.[spec.id] ?? index * 10
|
|
104
260
|
if (!Number.isSafeInteger(sequence) || sequence < 0) throw new TypeError(`rule ${spec.id}: invalid configOrder`)
|
|
105
|
-
const when = compileWhen(ruleIf,
|
|
261
|
+
const when = compileWhen(ruleIf, conditionOptions)
|
|
262
|
+
// 规则级 else 依赖 `not(if)` 才能与 then 互斥;没有 if 时 else 会退化成无条件动作。
|
|
263
|
+
if (spec.else !== undefined && (ruleIf === undefined || ruleIf === null)) {
|
|
264
|
+
throw new TypeError(`rule ${spec.id}: else requires an if — 规则级 else 不能脱离 if 存在`)
|
|
265
|
+
}
|
|
106
266
|
// 规则级 `if` 由 `rule.when` 判定,then 分支的动作不再重复叠加它;规则级 `else` 的动作
|
|
107
267
|
// 自带 `not(if)` 并标记跳过规则级判定——否则两者自相矛盾,else 分支永远不会执行。
|
|
108
268
|
const expanded = [
|
|
109
269
|
...expandActions(ruleThen, {}, `rule ${spec.id}.then`),
|
|
110
270
|
...(spec.else === undefined ? [] : expandActions(spec.else, {
|
|
111
|
-
outer:
|
|
271
|
+
outer: [ruleNegation],
|
|
112
272
|
bypass: true,
|
|
113
273
|
}, `rule ${spec.id}.else`)),
|
|
114
274
|
]
|
|
@@ -118,13 +278,50 @@ export function compileRules(specs, options = {}) {
|
|
|
118
278
|
if (actionIds.has(source.id)) throw new TypeError(`rule ${spec.id}: duplicate action id ${source.id}`)
|
|
119
279
|
actionIds.add(source.id)
|
|
120
280
|
if (!Object.hasOwn(ACTION_KINDS, source.kind)) throw new TypeError(`rule ${spec.id}: unknown action ${source.kind}`)
|
|
281
|
+
// 带 `kind` 的节点同时写分支键时,`expandActions` 只认无 `kind` 的分支节点——分支会被
|
|
282
|
+
// 静默丢掉(条件不生效、动作照旧执行),因此点名拒绝而不是当未知键。
|
|
283
|
+
const branchKeys = ['if', 'then', 'else'].filter(key => source[key] !== undefined)
|
|
284
|
+
if (branchKeys.length > 0) throw new TypeError(`rule ${spec.id}: action ${source.id} 写了 ${branchKeys.join(', ')} — 分支必须写成无 \`kind\` 的节点`)
|
|
285
|
+
// 旧动作级开关已退役:显式拒绝并给出新名(比 unknown fields 更可直接照做)。
|
|
286
|
+
if (source.when !== undefined) throw new TypeError(`rule ${spec.id}: action ${source.id}: "when" 已退役,改用 "if"`)
|
|
287
|
+
if (source.prepend !== undefined) throw new TypeError(`rule ${spec.id}: action ${source.id}: ${PREPEND_RETIRED}`)
|
|
288
|
+
// 声明路径(YAML / JSON / 导入)表达不了函数,而 `structuredClone` 对函数抛
|
|
289
|
+
// DataCloneError——报错提示「改用函数」等于把人引向死路。`match` 出现即拒,
|
|
290
|
+
// 条件写在规则级 `if` 或分支节点里。
|
|
291
|
+
if (source.match !== undefined) {
|
|
292
|
+
throw new TypeError(`rule ${spec.id}: action ${source.id}.match 已取消 — 声明路径写不了函数,条件改用 rule.if`)
|
|
293
|
+
}
|
|
121
294
|
const action = structuredClone(source)
|
|
122
295
|
if (action.kind === 'inject-text') {
|
|
123
296
|
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
297
|
}
|
|
126
298
|
const execution = ruleActionExecution(action)
|
|
299
|
+
// 未知键检查必须在 normalizeActionGates **之后**:中性旧 audience/modelScope/promotion
|
|
300
|
+
// 由它删除、非中性旧门由它报「move it to rule.if」,那些键因此既不会落在未知键名单里,
|
|
301
|
+
// 也不会被静默丢弃。声明式 config.subject/match 已删除,不再进入这张旧门名单。
|
|
127
302
|
normalizeActionGates(action, execution)
|
|
303
|
+
validateConfig(`rule ${spec.id}: action ${action.id}`, action, ACTION_FIELDS[action.kind])
|
|
304
|
+
if (action.kind === 'request-params') {
|
|
305
|
+
// patch / unset 的子键同属 LlmCallConfig(键集与值规则复用 schema 的 assertLlmCallPatch):
|
|
306
|
+
// 拼错的键与非法值今天会被静默忽略(补丁不生效),与动作级未知键是同一类失效。
|
|
307
|
+
for (const field of ['patch', 'unset']) {
|
|
308
|
+
if (record(action[field])) assertLlmCallPatch(action[field], `action ${action.id}.${field}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (action.kind === 'inject-text') {
|
|
312
|
+
if (action.config.prepend !== undefined) throw new TypeError(`rule ${spec.id}: action ${action.id}: ${PREPEND_RETIRED}`)
|
|
313
|
+
validateConfig(`rule ${spec.id}: action ${action.id} config`, action.config, INJECT_CONFIG_FIELDS)
|
|
314
|
+
// F27:complete / suppressRuntimeContext 是 system-section 的注册期效果。写在别的层上
|
|
315
|
+
// 既不注册成独占段、又会被独占计数误算,逐动作点名拒绝(params 子键不做白名单)。
|
|
316
|
+
// 出现即拒(`false` 除外):`'yes'` / `1` 这类值同样落不到任何注册期效果上。
|
|
317
|
+
if (action.config.layer !== 'system-section' && record(action.config.params)) {
|
|
318
|
+
for (const flag of ['complete', 'suppressRuntimeContext']) {
|
|
319
|
+
if (action.config.params[flag] !== undefined && action.config.params[flag] !== false) {
|
|
320
|
+
throw new TypeError(`rule ${spec.id}: action ${action.id}: config.params.${flag} 只属于 layer system-section — 当前 layer 是 ${JSON.stringify(action.config.layer)}`)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
128
325
|
const channelOrder = action.channelOrder ?? sequence
|
|
129
326
|
if (!Number.isSafeInteger(channelOrder) || channelOrder < 0) throw new TypeError(`action ${action.id}: channelOrder must be a non-negative safe integer`)
|
|
130
327
|
const waterfallPosition = action.waterfallPosition ?? 'default'
|
|
@@ -134,16 +331,25 @@ export function compileRules(specs, options = {}) {
|
|
|
134
331
|
if (action.kind === 'inject-text') {
|
|
135
332
|
validateActionOptions(action)
|
|
136
333
|
if (!record(action.config)) throw new TypeError(`action ${action.id}: config must be an object`)
|
|
137
|
-
//
|
|
138
|
-
pendingConfigs.push({ action, source: { ...action.config
|
|
334
|
+
// 原始声明保持不变;正文身份由 `injectionConfigSpec` 保证(`config.id` 非空,不再兜第二次)。
|
|
335
|
+
pendingConfigs.push({ action, source: { ...action.config }, sequence: channelOrder, ruleId: spec.id, actionIndex })
|
|
139
336
|
} else prepareAction(action, { promptConfigOptions: options.promptConfigOptions })
|
|
337
|
+
// 动作级条件只在本动作的执行点求值:谓词要读的事实必须由该通道真实提供,否则是死条件。
|
|
338
|
+
// 规则级 `if` 自身不在这里校验(它跨执行点求值);`else` 注入的 `not(if)` 按**对象身份**
|
|
339
|
+
// 剔除——因此嵌套分支的 `if` 照样校验,同名 subject 不再被规则级 `if` 连带跳过。
|
|
340
|
+
const actionConditions = conjunction(conditions)
|
|
341
|
+
const ownConditions = conjunction(conditions.filter(node => node !== ruleNegation))
|
|
342
|
+
for (const { predicate, subject } of conditionSubjects(ownConditions)) {
|
|
343
|
+
assertSubject(predicate, subject,
|
|
344
|
+
predicate === 'text' ? channelTextSubjects(execution.channel) : channelFactSubjects(execution.channel),
|
|
345
|
+
`rule ${spec.id}: action ${action.id}`)
|
|
346
|
+
}
|
|
140
347
|
// 空条件数组经 conjunction → undefined → compileWhen 直接返回 undefined(无条件动作)。
|
|
141
|
-
const actionWhen = compileWhen(
|
|
142
|
-
|
|
143
|
-
|
|
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`)
|
|
348
|
+
const actionWhen = compileWhen(actionConditions, conditionOptions)
|
|
349
|
+
if (actionWhen !== undefined && execution.lifecycle === 'registration') {
|
|
350
|
+
throw new TypeError(`action ${action.id}: registration effects do not support conditional branches`)
|
|
145
351
|
}
|
|
146
|
-
return Object.assign(action, { execution, channelOrder, waterfallPosition, actionIndex, actionWhen, bypassRuleWhen: bypass })
|
|
352
|
+
return Object.assign(action, { execution, channelOrder, waterfallPosition, actionIndex, actionWhen, bypassRuleWhen: bypass, conditions: structuredClone(actionConditions) })
|
|
147
353
|
})
|
|
148
354
|
const pointOrders = new Map()
|
|
149
355
|
for (const action of actions) {
|
|
@@ -152,23 +358,91 @@ export function compileRules(specs, options = {}) {
|
|
|
152
358
|
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
359
|
pointOrders.set(key, action)
|
|
154
360
|
}
|
|
155
|
-
return {
|
|
361
|
+
return {
|
|
362
|
+
id: spec.id, name: spec.name, layer: spec.layer, group: spec.group, exclusive: spec.exclusive === true, enabled: spec.enabled !== false,
|
|
363
|
+
sequence, when, actions, promptConfigOptions: options.promptConfigOptions,
|
|
364
|
+
// 绑定所需的树:规则级 `if` + 每个动作展开后的条件(动作那半与 `action.conditions` 同一引用)。
|
|
365
|
+
// **只有引擎内部读它**(`rebindConditions` 绑定、`actionAudiences` 推导受众)——调用方与挂载缝都不需要知道这个形状。
|
|
366
|
+
conditionTrees: {
|
|
367
|
+
...(ruleIf === undefined ? {} : { when: structuredClone(ruleIf) }),
|
|
368
|
+
actions: Object.fromEntries(actions.filter(action => action.conditions !== undefined).map(action => [action.id, action.conditions])),
|
|
369
|
+
},
|
|
370
|
+
}
|
|
156
371
|
})
|
|
157
372
|
validateRuleGroups(rules)
|
|
158
|
-
|
|
159
|
-
|
|
373
|
+
for (const audience of ['main', 'subagent']) {
|
|
374
|
+
const complete = completeRuleSections(rules, audience)
|
|
375
|
+
if (complete.length > 1 || (complete.length > 0 && options.personaComplete === true)) {
|
|
376
|
+
throw new TypeError(`rules: multiple complete system sections are active for ${audience}: ${complete.join(', ')}`)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
160
379
|
if (pendingConfigs.length) {
|
|
161
380
|
const compiled = createPromptConfigs(pendingConfigs.map(item => item.source), { ...options.promptConfigOptions, ...(moduleId === undefined ? {} : { sourceModuleId: moduleId }) })
|
|
162
381
|
const byId = new Map(compiled.map(config => [config.id, config]))
|
|
163
382
|
for (const item of pendingConfigs) {
|
|
164
383
|
const config = byId.get(item.source.id)
|
|
165
|
-
Object.assign(config, { sequence: item.sequence, ruleId: item.ruleId, ruleActionIndex: item.actionIndex })
|
|
384
|
+
Object.assign(config, { sequence: item.sequence, ruleId: item.ruleId, ruleActionId: item.action.id, ruleActionIndex: item.actionIndex })
|
|
385
|
+
// 停用标记在这里打,不在 `injectionConfigSpec`:那是校验**之前**,标记会变成作者可写的字段。
|
|
386
|
+
// 校验之后打标,作者手写同名键按未知键 fail loud;执行期据此跳过会话变量合并。
|
|
387
|
+
if (options.variablesEnabled === false) config[SESSION_VARIABLES_DISABLED] = true
|
|
388
|
+
// 模块级 ST 递归开关(module.yml 顶层):与上一行同源同时序,作者写同名键按未知键 fail loud。
|
|
389
|
+
if (options.contextActivationRecursive === true) config.contextActivationRecursive = true
|
|
166
390
|
item.action.compiledConfig = config
|
|
167
391
|
}
|
|
168
392
|
}
|
|
393
|
+
for (const rule of rules) validateActionLinks(rule)
|
|
394
|
+
validateDeliverySources([{ moduleId, rules }])
|
|
169
395
|
return rules
|
|
170
396
|
}
|
|
171
397
|
|
|
398
|
+
/**
|
|
399
|
+
* 树里是否需要挂载期的输入:
|
|
400
|
+
* - `preset` 谓词:`conditions/preset.mjs` 把服务查询基准绑在**构造期**;
|
|
401
|
+
* - `phase` 谓词(且未显式 `subscribe: false`):订阅模式要从挂载缝的追踪器池里取实例
|
|
402
|
+
* (见 `promotion-pool.mjs`),否则每个编译出的谓词各建一份、各自订阅、各自喂事件。
|
|
403
|
+
* - `anchor.after`:引用目标需要挂载来源的真实模块身份。
|
|
404
|
+
*
|
|
405
|
+
* 其余原语只用节点选项。只重绑需要重绑的树:调用方对不含这些节点的条件谓词做的包装/替换
|
|
406
|
+
* 不会被覆盖,「挂载缝接管绑定」与「调用方仍可注入自定义判定」两件事不冲突。
|
|
407
|
+
*/
|
|
408
|
+
function needsMountContext(node) {
|
|
409
|
+
if (node === null || typeof node !== 'object') return false
|
|
410
|
+
if (Array.isArray(node)) return node.some(needsMountContext)
|
|
411
|
+
if (Object.hasOwn(node, 'preset')) return true
|
|
412
|
+
// variable 要读挂载 scope 的变量适配器(准备期没有)。
|
|
413
|
+
if (Object.hasOwn(node, 'variable')) return true
|
|
414
|
+
if (Object.hasOwn(node, 'phase') && node.phase?.subscribe !== false) return true
|
|
415
|
+
if (node.anchor?.after !== undefined) return true
|
|
416
|
+
return Object.values(node).some(needsMountContext)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 把需要挂载期 ctx 的条件绑定到挂载缝:**`compileRules` 只编译与校验,绑定由 `mountRuleSources`
|
|
421
|
+
* 在这里完成**(`rule-runtime.mjs`)。
|
|
422
|
+
*
|
|
423
|
+
* 为什么不在编译期绑:`preset` 谓词要挂载 scope 的 ctx,准备期拿不到——编译期绑出来的 preset
|
|
424
|
+
* 条件恒 UNAVAILABLE,而且是静默的。树只在本模块读:调用方与挂载缝都不需要知道
|
|
425
|
+
* `conditionTrees` 的形状;没有该字段的产物(已绑好的输入)原样保留。
|
|
426
|
+
*/
|
|
427
|
+
export function rebindConditions(sources, context = {}) {
|
|
428
|
+
if (!Array.isArray(sources)) throw new TypeError('sources must be an array')
|
|
429
|
+
for (const source of sources) {
|
|
430
|
+
for (const rule of source?.rules ?? []) {
|
|
431
|
+
const trees = rule.conditionTrees
|
|
432
|
+
if (trees === undefined) continue
|
|
433
|
+
const targets = new Map(rule.actions.filter(action => action.compiledConfig !== undefined)
|
|
434
|
+
.map(action => [action.id, { ...action.compiledConfig, sourceModuleId: source.moduleId }]))
|
|
435
|
+
const binding = { ...context, moduleId: source.moduleId, resolveAction: id => targets.get(id) }
|
|
436
|
+
if (trees.when !== undefined && needsMountContext(trees.when)) rule.when = compileWhen(trees.when, binding)
|
|
437
|
+
for (const action of rule.actions ?? []) {
|
|
438
|
+
const tree = trees.actions?.[action.id]
|
|
439
|
+
if (tree !== undefined && needsMountContext(tree)) action.actionWhen = compileWhen(tree, binding)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return sources
|
|
444
|
+
}
|
|
445
|
+
|
|
172
446
|
export function getRuleEditorMeta() {
|
|
173
447
|
const predicates = Object.keys(PREDICATE_FACTORIES).map(kind => ({ kind, example: { [kind]: structuredClone(PREDICATE_EXAMPLES[kind]) } }))
|
|
174
448
|
for (const kind of COMPOSITE_OPERATORS) {
|
package/engine/run-code-env.mjs
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { bool, defineConfig, stringList } from './fields.mjs'
|
|
22
|
+
import { machinePaths } from './machine-paths.mjs'
|
|
23
|
+
import { SENSITIVE_ENV_RE } from './env-safety.mjs'
|
|
22
24
|
|
|
23
25
|
/** Cordis 插件名,供 loader 诊断使用。 */
|
|
24
26
|
export const name = 'run-code-env'
|
|
@@ -26,11 +28,15 @@ export const name = 'run-code-env'
|
|
|
26
28
|
/** 需要 tools 视图与 systemPrompt 段。shellEnv 为机会型读取,不写进 inject。 */
|
|
27
29
|
export const inject = ['tools', 'systemPrompt']
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
export
|
|
31
|
+
/** 任何疑似凭据的名字一律不放行;判据与 env-facts 共用 env-safety。 */
|
|
32
|
+
export { SENSITIVE_ENV_RE }
|
|
31
33
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* run_code 工具对象上的共享状态:官方 ToolRuntime 的 run_code 是进程内单例,多个 Agent
|
|
36
|
+
* 共用同一个定义对象。状态随对象走,跨模块实例/HMR 复用;配置在每次执行时按 Agent 查,
|
|
37
|
+
* 不能被某次 apply 的闭包固化。
|
|
38
|
+
*/
|
|
39
|
+
const STATE = Symbol.for('dsh-plugin-prompt-tool.run-code-env')
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* envKeys 字段:非空字符串数组,逐项 trim 后去重。
|
|
@@ -126,16 +132,15 @@ export function buildEnv(ctx, keys, agent) {
|
|
|
126
132
|
// shellEnv 缺失/失败只意味着没有受管变量,不阻断 PTC。
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
// DSH_HOME
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
if (env.DSH_HOME === undefined) {
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
env.DSH_HOME = String(ambientHome)
|
|
136
|
-
}
|
|
135
|
+
// DSH_HOME 兜底:受管快照没给值时按机器路径事实的**唯一来源**推导(与插值同答案:
|
|
136
|
+
// 显式环境变量 → `USERPROFILE\.dsh`)。这是有意的**行为对齐**——此前未设 DSH_HOME 时
|
|
137
|
+
// 这里不给该键,于是 `{{DSH_HOME}}` 与 `run_code` 的 `env.DSH_HOME` 是两个答案。
|
|
138
|
+
if (env.DSH_HOME === undefined && !SENSITIVE_ENV_RE.test('DSH_HOME')) {
|
|
139
|
+
const derived = machinePaths(agent?.session, hostEnv).DSH_HOME
|
|
140
|
+
if (derived.length > 0) env.DSH_HOME = derived
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
// DSH_WORKSPACE 只认**会话 cwd**(没有就不落键)——取值统一后仍按本处规则决定给不给键。
|
|
139
144
|
const cwd = agent?.session?.header?.cwd
|
|
140
145
|
if (typeof cwd === 'string' && cwd.length > 0 && !SENSITIVE_ENV_RE.test('DSH_WORKSPACE')) {
|
|
141
146
|
env.DSH_WORKSPACE = cwd
|
|
@@ -169,38 +174,84 @@ function envSectionText(ctx, keys) {
|
|
|
169
174
|
].join('\n')
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
177
|
+
/** 取或建立工具状态;`configs` 按真实调用 Agent 键控,default 服务无 agent 的 apply。 */
|
|
178
|
+
function stateOf(tool) {
|
|
179
|
+
return (tool[STATE] ??= { configs: new WeakMap(), default: undefined, patched: false })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 给 run_code 的 ToolDefinition 打补丁:execute 前注入 env 前缀。
|
|
184
|
+
* 单例只包装一次;配置在每次执行时读取,不再固化首个 Agent 的 ctx/keys。
|
|
185
|
+
*/
|
|
186
|
+
export function patchRunCodeTool(tool) {
|
|
174
187
|
if (tool === undefined || tool.name !== 'run_code') return false
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
const state = stateOf(tool)
|
|
189
|
+
if (state.patched) return false
|
|
177
190
|
|
|
178
191
|
const originalExecute = tool.execute.bind(tool)
|
|
179
192
|
tool.execute = async (rawArgs, exec) => {
|
|
193
|
+
const agent = exec?.agent
|
|
194
|
+
// 具名 Agent 只用自己的注册;无 agent 的执行才回落到 default 配置。
|
|
195
|
+
const config = agent === undefined ? state.default : state.configs.get(agent)
|
|
196
|
+
if (config === undefined) return originalExecute(rawArgs, exec)
|
|
197
|
+
// 官方在策略前已深冻结 arguments,不能原地改写;spread 出新对象再交给原 execute。
|
|
180
198
|
const args = rawArgs ?? {}
|
|
181
|
-
const env = buildEnv(ctx, keys,
|
|
199
|
+
const env = buildEnv(config.ctx, config.keys, agent)
|
|
182
200
|
const code = injectEnvPrefix(typeof args.code === 'string' ? args.code : '', env)
|
|
183
201
|
return originalExecute({ ...args, code }, exec)
|
|
184
202
|
}
|
|
203
|
+
state.patched = true
|
|
185
204
|
return true
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
/**
|
|
208
|
+
* 登记一份 Agent 配置:同一 Agent 多次 apply(多个模块)时 keys 取并集、ctx 取最新。
|
|
209
|
+
* @returns 注销函数;只删除自己仍是当前配置的那份,后注册的并集不被先撤者抹掉。
|
|
210
|
+
*/
|
|
211
|
+
function configureRunCode(tool, ctx, keys, agent) {
|
|
212
|
+
const state = stateOf(tool)
|
|
213
|
+
const previous = agent === undefined ? state.default : state.configs.get(agent)
|
|
214
|
+
const entry = { ctx, keys: new Set([...(previous?.keys ?? []), ...keys]) }
|
|
215
|
+
if (agent === undefined) state.default = entry
|
|
216
|
+
else state.configs.set(agent, entry)
|
|
217
|
+
return () => {
|
|
218
|
+
const current = agent === undefined ? state.default : state.configs.get(agent)
|
|
219
|
+
if (current !== entry) return
|
|
220
|
+
if (agent === undefined) state.default = undefined
|
|
221
|
+
else state.configs.delete(agent)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
188
225
|
/** 注册 PTC env 注入:只在目标 scope 出现 run_code 时生效。 */
|
|
189
|
-
export function apply(ctx, config) {
|
|
226
|
+
export function apply(ctx, config, options = {}) {
|
|
190
227
|
// 校验入口:未知键 / enabled 非布尔 / envKeys 缺失或全空都在挂载期 fail loud。
|
|
191
228
|
const source = configContract.parse(config, name)
|
|
192
229
|
// 开关语义:未声明 = 关闭(组合源为本模块显式写 enabled: true)。
|
|
193
230
|
const { enabled, envKeys: keys } = source
|
|
231
|
+
const agent = options?.agent
|
|
232
|
+
let disposeConfig
|
|
233
|
+
let configuredTool
|
|
194
234
|
|
|
195
235
|
const tryPatch = (scope) => {
|
|
196
236
|
if (!enabled) return
|
|
197
237
|
try {
|
|
198
|
-
|
|
238
|
+
const tool = ctx.tools.get('run_code', scope)
|
|
239
|
+
if (tool === undefined || tool === null) return
|
|
240
|
+
patchRunCodeTool(tool)
|
|
241
|
+
// 同一工具只登记一次;若宿主替换了 transport 定义,旧配置让位给新工具。
|
|
242
|
+
if (tool !== configuredTool) {
|
|
243
|
+
disposeConfig?.()
|
|
244
|
+
disposeConfig = configureRunCode(tool, ctx, keys, agent)
|
|
245
|
+
configuredTool = tool
|
|
246
|
+
}
|
|
199
247
|
} catch {
|
|
200
248
|
// run_code 尚未物化或 registry 不可用:下一次 assemble/execute 再试。
|
|
201
249
|
}
|
|
202
250
|
}
|
|
203
251
|
|
|
252
|
+
// 当前 scope 已物化 run_code 时挂载期即可登记。
|
|
253
|
+
tryPatch(agent)
|
|
254
|
+
|
|
204
255
|
// 动态工具变化后,新 scope 的 run_code transport 可能刚创建。
|
|
205
256
|
ctx.on('tools/change', () => tryPatch())
|
|
206
257
|
|
|
@@ -228,4 +279,7 @@ export function apply(ctx, config) {
|
|
|
228
279
|
},
|
|
229
280
|
})
|
|
230
281
|
}
|
|
282
|
+
|
|
283
|
+
// 释放时只撤本模块登记的配置;工具上的 wrapper 保留(单例、无配置即透传)。
|
|
284
|
+
return enabled ? () => { disposeConfig?.() } : undefined
|
|
231
285
|
}
|