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/layers.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* layers — 非 pre-step
|
|
2
|
+
* layers — 非 pre-step 的官方层级接线。
|
|
3
3
|
* 所有注册都只作用于本插件提示词配置;单条失败 warnOnce 后继续。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
MAX_TRACKED_SESSIONS,
|
|
8
|
-
extractText,
|
|
9
8
|
getService,
|
|
10
9
|
isDelegated,
|
|
11
10
|
keepDisposer,
|
|
11
|
+
matchesAudience,
|
|
12
12
|
matchesModel,
|
|
13
13
|
newMessageId,
|
|
14
|
-
parseToolNames,
|
|
15
14
|
sessionState,
|
|
16
15
|
} from './shared.mjs'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
16
|
+
import { pluginKind } from './source-kind.mjs'
|
|
17
|
+
import { INJECTION_LAYERS, KNOWN_STRATEGIES, SESSION_VARIABLES_DISABLED } from './schema.mjs'
|
|
18
|
+
import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue, isReservedInterpolationName } from './interpolate.mjs'
|
|
19
|
+
import { renderTemplate } from './template-frame.mjs'
|
|
21
20
|
import { compareConfigSequence, compareTextPlacement } from './order.mjs'
|
|
22
|
-
import { ruleFrame,
|
|
21
|
+
import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
|
|
22
|
+
import { textRegistrationName, identityStampOf, actionStamp, injectedMessage, findActionDelivery } from './delivery.mjs'
|
|
23
23
|
|
|
24
24
|
const name = 'prompt-config-engine'
|
|
25
25
|
// 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
|
|
@@ -33,11 +33,14 @@ const subagentEndDeliveries = new WeakMap()
|
|
|
33
33
|
* keep=官方变量通道按 assembly 求值的名字,静态 pass 命中即原样保留引用。 */
|
|
34
34
|
function configText(config, registries, warnOnce, context) {
|
|
35
35
|
const registry = registries.get(config)
|
|
36
|
-
if (
|
|
37
|
-
return officialChannelText(config
|
|
36
|
+
if (config.params?.templateEvaluation === true) {
|
|
37
|
+
try { return officialChannelText(renderTemplate(config, context, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce }), `${config.layer} ${config.id}`, registry, warnOnce) }
|
|
38
|
+
catch (error) { warnOnce(`${name}: template ${config.id} failed: ${String(error?.message ?? error)}`); return '' }
|
|
38
39
|
}
|
|
40
|
+
const variables = config[SESSION_VARIABLES_DISABLED] === true ? {} : config.texts.some(text => text.includes('{{'))
|
|
41
|
+
? config.variableStore?.read(config.sourceModuleId).effective ?? config.variables : config.variables
|
|
39
42
|
const text = config.texts
|
|
40
|
-
.map((item) => interpolateVariables(item,
|
|
43
|
+
.map((item, index) => interpolateVariables(item, variables, context?.agent?.session, registry?.protect, `${config.id}#${index}`))
|
|
41
44
|
.filter((item) => item.length > 0)
|
|
42
45
|
.join('\n\n')
|
|
43
46
|
return officialChannelText(text, `${config.layer} ${config.id}`, registry, warnOnce)
|
|
@@ -76,7 +79,8 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
|
76
79
|
const reserved = new Set()
|
|
77
80
|
const facts = new Set()
|
|
78
81
|
for (const config of configs) {
|
|
79
|
-
|
|
82
|
+
// 持久值在各正文 provider 被调用时读取,使后卡看到前卡刚提交的变量。
|
|
83
|
+
if (config.params?.templateEvaluation === true || config.variableStore !== undefined) continue
|
|
80
84
|
const referenced = new Set()
|
|
81
85
|
for (const text of config.texts) {
|
|
82
86
|
for (const match of String(text).matchAll(NAME_REFERENCE_RE)) referenced.add(match[1])
|
|
@@ -93,14 +97,14 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
|
93
97
|
for (const [config, referenced] of references) {
|
|
94
98
|
const registry = registries.get(config)
|
|
95
99
|
for (const name of referenced) {
|
|
96
|
-
const declared = Object.hasOwn(config.variables, name)
|
|
100
|
+
const declared = Object.hasOwn(config.variables, name) || config.variableStore !== undefined && !isReservedInterpolationName(name)
|
|
97
101
|
const isFact = !declared && RUNTIME_FACTS.has(name.toLowerCase())
|
|
98
102
|
if (!declared && !isFact) continue
|
|
99
103
|
let official = isFact ? name.toLowerCase() : OFFICIAL_NAME_RE.test(name) ? name : officialAliasOf(name)
|
|
100
104
|
try {
|
|
101
105
|
// 内容绑定包含局部命名空间:会话覆盖也可以再引用其中的变量。
|
|
102
106
|
// 纯事实不属于任何配置的内容命名空间,所有大小写变体复用规范名。
|
|
103
|
-
const signature = isFact ? `fact:${official}` : JSON.stringify([name, Object.entries(config.variables).sort(([a], [b]) => a.localeCompare(b))])
|
|
107
|
+
const signature = isFact ? `fact:${official}` : JSON.stringify([config.sourceModuleId, name, config.variableStore === undefined ? Object.entries(config.variables).sort(([a], [b]) => a.localeCompare(b)) : null])
|
|
104
108
|
let binding = bindings.get(signature)
|
|
105
109
|
if (binding === undefined) {
|
|
106
110
|
if (usedNames.has(official) || (!isFact && facts.has(official)) || (official !== name && !isFact && reserved.has(official))) {
|
|
@@ -110,12 +114,13 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
|
110
114
|
}
|
|
111
115
|
usedNames.add(official)
|
|
112
116
|
const source = isFact ? name.toLowerCase() : name
|
|
113
|
-
const
|
|
117
|
+
const variablesDisabled = config[SESSION_VARIABLES_DISABLED] === true
|
|
114
118
|
keep(systemPrompt.variable(official, (context) => {
|
|
115
119
|
const session = context?.agent?.session
|
|
116
|
-
const
|
|
117
|
-
const value =
|
|
118
|
-
|
|
120
|
+
const variables = variablesDisabled ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables
|
|
121
|
+
const value = Object.hasOwn(variables, source) ? String(variables[source]) : runtimeFactValue(source, session) ?? ''
|
|
122
|
+
// 变量值内的 {{pick}} 同样要有位置身份(配置 id + 声明名),否则同会话下多个变量的首个 pick 撞值。
|
|
123
|
+
const expanded = interpolateVariables(value, variables, session, undefined, `variable:${config.sourceModuleId}:${config.id}:${name}`)
|
|
119
124
|
return officialChannelText(expanded, `variable ${official}`, { alias: new Map(), registered: new Set() }, warnOnce)
|
|
120
125
|
}), `${name}: official prompt variable ${official}`)
|
|
121
126
|
binding = official
|
|
@@ -141,16 +146,14 @@ function rewriteOfficialAliases(text, alias) {
|
|
|
141
146
|
/** agent/request 与 tools/* 层的作用域过滤。 */
|
|
142
147
|
export function matchesAgentScope(config, agent) {
|
|
143
148
|
if (agent === undefined) return true
|
|
144
|
-
|
|
145
|
-
if (config.audience === 'main' && delegated) return false
|
|
146
|
-
if (config.audience === 'subagent' && !delegated) return false
|
|
149
|
+
if (!matchesAudience(config.audience, isDelegated(agent.session) ? 'subagent' : 'main')) return false
|
|
147
150
|
return matchesModel(config.modelScope, agent.options?.model)
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
/**
|
|
151
154
|
* 官方插值通道(system-section / runtime-context)出口清洗:本项目解析后仍残留的
|
|
152
155
|
* 引用一律剥离,官方严格插值因此看不到未注册引用(既不抛错也不留字面)。
|
|
153
|
-
* 只在本项目的宽容解析之后调用;其他层(pre-step / agent-request
|
|
156
|
+
* 只在本项目的宽容解析之后调用;其他层(pre-step / agent-request)
|
|
154
157
|
* 不经官方插值,保持原有宽容语义。
|
|
155
158
|
*/
|
|
156
159
|
function officialChannelText(text, label, registry, warnOnce) {
|
|
@@ -171,36 +174,27 @@ function textLayerGroups(configs) {
|
|
|
171
174
|
const base = previous?.[0]
|
|
172
175
|
if (config.mergeMode === 'merged' && base?.mergeMode === 'merged'
|
|
173
176
|
&& config.sourceModuleId === base.sourceModuleId
|
|
177
|
+
&& (config.params?.complete === true) === (base.params?.complete === true)
|
|
178
|
+
&& (config.params?.suppressRuntimeContext === true) === (base.params?.suppressRuntimeContext === true)
|
|
174
179
|
&& config.position === base.position && config.order === base.order) previous.push(config)
|
|
175
180
|
else groups.push([config])
|
|
176
181
|
}
|
|
177
182
|
return groups
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
/** 官方同 order 按 name 排序:默认名编码安全整数序号,显式名仍由作者拥有。 */
|
|
181
|
-
function textRegistrationName(config, field) {
|
|
182
|
-
const explicit = config.params?.[field]
|
|
183
|
-
if (typeof explicit === 'string' && explicit.length > 0) return explicit
|
|
184
|
-
if (config.sourceModuleId === undefined) return config.id
|
|
185
|
-
// 非负安全整数最多 16 位,跨过四位文件前缀后仍保持数值顺序。
|
|
186
|
-
const sequence = String(config.sequence).padStart(16, '0')
|
|
187
|
-
return `prompt-tool:${sequence}:${config.sourceModuleId}:${config.id}`
|
|
188
|
-
}
|
|
189
|
-
|
|
190
185
|
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
191
|
-
function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
|
|
192
|
-
const disposers = []
|
|
186
|
+
function wireSystemSections(ctx, configs, registry, warnOnce, keep, ordered) {
|
|
193
187
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
194
188
|
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
195
189
|
if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — system-section configs skipped`)
|
|
196
|
-
return
|
|
190
|
+
return
|
|
197
191
|
}
|
|
198
192
|
// 人设段(deployment:persona-prefix/suffix)由官方 @deepseek-ai/dsh-persona 行注册
|
|
199
193
|
// (preset.yml 顶层 persona 段驱动);本层只处理其余 system-section 配置。
|
|
200
194
|
for (const group of textLayerGroups(configs)) {
|
|
201
195
|
const base = group[0]
|
|
202
196
|
try {
|
|
203
|
-
const dynamic = group.some((config) => config.audience != null ||
|
|
197
|
+
const dynamic = group.some((config) => config.audience != null || config.params?.templateEvaluation === true || config.variableStore !== undefined
|
|
204
198
|
|| config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
|
|
205
199
|
const groupText = dynamic ? '' : group.map((config) => configText(config, registry, warnOnce)).filter((item) => item.length > 0).join('\n\n')
|
|
206
200
|
if (!dynamic && groupText.length === 0) continue
|
|
@@ -211,12 +205,14 @@ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
|
|
|
211
205
|
.filter((item) => item.length > 0)
|
|
212
206
|
.join('\n\n')
|
|
213
207
|
: groupText
|
|
208
|
+
const entryName = textRegistrationName(base, 'sectionName')
|
|
214
209
|
keep(systemPrompt.section({
|
|
215
|
-
name:
|
|
210
|
+
name: entryName,
|
|
216
211
|
order: base.order,
|
|
217
212
|
text,
|
|
218
213
|
...(base.params?.complete === true ? { complete: true } : {}),
|
|
219
214
|
}), `${name}: section ${base.id}`)
|
|
215
|
+
ordered.sections.push(orderedEntry(entryName, base))
|
|
220
216
|
// suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
|
|
221
217
|
if (base.params?.suppressRuntimeContext === true) {
|
|
222
218
|
keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
|
|
@@ -225,7 +221,6 @@ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
|
|
|
225
221
|
warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
226
222
|
}
|
|
227
223
|
}
|
|
228
|
-
return disposers
|
|
229
224
|
}
|
|
230
225
|
|
|
231
226
|
async function resolvedContextText(ctx, config, context, registry, warnOnce) {
|
|
@@ -233,14 +228,19 @@ async function resolvedContextText(ctx, config, context, registry, warnOnce) {
|
|
|
233
228
|
const session = agent?.session
|
|
234
229
|
const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
|
|
235
230
|
if (context.signal?.aborted || resolved == null) return ''
|
|
236
|
-
const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
const variables = { ...(config[SESSION_VARIABLES_DISABLED] === true ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables), ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
232
|
+
if (config.params?.templateEvaluation === true) {
|
|
233
|
+
const texts = config.texts.length ? config.texts : [resolved.text ?? '']
|
|
234
|
+
return officialChannelText(renderTemplate({ ...config, texts }, context, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce }), `runtime-context ${config.id}`, registry, warnOnce)
|
|
235
|
+
}
|
|
236
|
+
// 模板位置身份 = 配置 id(texts 合并与 resolved.text 是同一位置的两个互斥分支);缺省会退化成 [会话, 出现序号]。
|
|
237
|
+
const rendered = config.texts.length > 0 ? interpolateVariables(config.texts.join('\n\n'), variables, session, undefined, config.id)
|
|
238
|
+
: typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session, undefined, config.id) : ''
|
|
239
239
|
return officialChannelText(rendered, `runtime-context ${config.id}`, registry, warnOnce)
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/** 条件化文本保留官方排序占位;仅在本次真实 assembly 填充,不缓存 context 身份。 */
|
|
243
|
-
function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
|
|
243
|
+
function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, ordered) {
|
|
244
244
|
if (!configs.length) return []
|
|
245
245
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
246
246
|
if (systemPrompt === undefined) { warnOnce(`${name}: systemPrompt unavailable`); return [] }
|
|
@@ -254,6 +254,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
254
254
|
const entryName = textRegistrationName(base, section ? 'sectionName' : 'contextName')
|
|
255
255
|
keep(systemPrompt[section ? 'section' : 'context']({ name: entryName, order: base.order, text: marker }), `${name}: conditional ${entryName}`)
|
|
256
256
|
bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts' })
|
|
257
|
+
ordered[section ? 'sections' : 'contexts'].push(orderedEntry(entryName, base))
|
|
257
258
|
}
|
|
258
259
|
let active = true
|
|
259
260
|
keep(() => { active = false }, `${name}: conditional text lifecycle`)
|
|
@@ -265,7 +266,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
265
266
|
entry.text = ''
|
|
266
267
|
const blocks = []
|
|
267
268
|
for (const config of binding.group) {
|
|
268
|
-
if (!active || context.signal?.aborted || !
|
|
269
|
+
if (!active || context.signal?.aborted || !actionMatches(config, frame) || !matchesAgentScope(config, context.agent)) continue
|
|
269
270
|
try {
|
|
270
271
|
const text = config.layer === 'runtime-context' && (config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
|
|
271
272
|
? await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
|
|
@@ -281,7 +282,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
/** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
|
|
284
|
-
function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
|
|
285
|
+
function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on, ordered) {
|
|
285
286
|
const disposers = []
|
|
286
287
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
287
288
|
if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
|
|
@@ -295,17 +296,20 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
295
296
|
for (const group of textLayerGroups(staticConfigs)) {
|
|
296
297
|
const base = group[0]
|
|
297
298
|
try {
|
|
298
|
-
const dynamic = group.some(config =>
|
|
299
|
+
const dynamic = group.some(config => config.params?.templateEvaluation === true || config.variableStore !== undefined
|
|
299
300
|
|| config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
|
|
300
301
|
const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
|
|
301
302
|
const text = dynamic ? render : render()
|
|
302
303
|
if (!dynamic && text.length === 0) continue
|
|
304
|
+
const entryName = textRegistrationName(base, 'contextName')
|
|
303
305
|
keep(systemPrompt.context({
|
|
304
|
-
name:
|
|
306
|
+
name: entryName,
|
|
305
307
|
order: base.order,
|
|
306
308
|
text,
|
|
307
309
|
}), `${name}: context ${base.id}`)
|
|
310
|
+
ordered.contexts.push(orderedEntry(entryName, base))
|
|
308
311
|
} catch (error) {
|
|
312
|
+
if (base.linkedDelivery) throw error
|
|
309
313
|
warnOnce(`${name}: runtime-context config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
310
314
|
}
|
|
311
315
|
}
|
|
@@ -329,6 +333,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
329
333
|
text: slotText,
|
|
330
334
|
}), `${name}: context ${config.id}`)
|
|
331
335
|
registered.set(contextName, config)
|
|
336
|
+
ordered.contexts.push(orderedEntry(contextName, config))
|
|
332
337
|
} catch (error) {
|
|
333
338
|
warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
334
339
|
}
|
|
@@ -352,90 +357,6 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
|
|
|
352
357
|
return disposers
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
/**
|
|
356
|
-
* agent/request 载荷的唯一改写实现(B3 T2 第 (7) 类动作与既有 `agent-request`
|
|
357
|
-
* 层**共用这一份**,不另写一套请求改写)。三种语义:
|
|
358
|
-
* - `patch`:对下游已解析的 LlmCallConfig 做浅合并;
|
|
359
|
-
* - `replace: true`:整体替换为 `patch`(不叠加 base,故 `unset` 无比较对象);
|
|
360
|
-
* - `unset`:**按值条件删键** —— 仅当合并结果里的值**恰好等于**声明值时删除该键
|
|
361
|
-
* (`Object.is` 比较)。这来自原 `tool-bootstrap` 剥离 `bootstrapMaxTokens` 的语义
|
|
362
|
-
* (「仅当解析后的 maxTokens 恰好等于本声明注入的那个值时才删」),不是"存在即删"
|
|
363
|
-
* ——模型或其它插件设置的同名参数不会被删。
|
|
364
|
-
* 浅合并做不到删键,故删键必须走显式通道。
|
|
365
|
-
* @param params 提示词配置的 `params`(或动作的等价声明)。
|
|
366
|
-
* @param base 下游 `next()` 已解析的请求配置。
|
|
367
|
-
* @returns 改写后的请求配置;无 unset 命中时零额外分配。
|
|
368
|
-
*/
|
|
369
|
-
export function applyAgentRequestParams(params, base) {
|
|
370
|
-
const patch = params?.patch !== null && typeof params?.patch === 'object' && !Array.isArray(params.patch)
|
|
371
|
-
? params.patch
|
|
372
|
-
: {}
|
|
373
|
-
if (params?.replace === true) return { ...patch }
|
|
374
|
-
const merged = { ...base, ...patch }
|
|
375
|
-
const unset = params?.unset
|
|
376
|
-
if (unset === null || typeof unset !== 'object' || Array.isArray(unset)) return merged
|
|
377
|
-
let result
|
|
378
|
-
for (const [key, expected] of Object.entries(unset)) {
|
|
379
|
-
// 每个键都对着**合并结果**判定:一旦分配过副本,后续键不得跳过比较。
|
|
380
|
-
if (!Object.is(merged[key], expected)) continue
|
|
381
|
-
if (result === undefined) result = { ...merged }
|
|
382
|
-
delete result[key]
|
|
383
|
-
}
|
|
384
|
-
return result ?? merged
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
|
|
388
|
-
function wireAgentRequests(ctx, configs, warnOnce, on) {
|
|
389
|
-
if (configs.length === 0) return []
|
|
390
|
-
const ordered = configs.some(config => config.sequence !== undefined) ? configs : [...configs].reverse()
|
|
391
|
-
return [on('agent/request', async (payload, next, invocation) => {
|
|
392
|
-
let result = await next()
|
|
393
|
-
const frame = invocation ?? ruleFrame('agent/request', [payload], warnOnce)
|
|
394
|
-
// 模块请求在 next 返回后按序合并;无文件来源的独立调用保持原回栈顺序。
|
|
395
|
-
for (const config of ordered) {
|
|
396
|
-
try {
|
|
397
|
-
if (ruleMatches(config.rule, frame) && matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
|
|
398
|
-
} catch (error) {
|
|
399
|
-
warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return result
|
|
403
|
-
})]
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
|
|
407
|
-
async function* replacedStream(text) {
|
|
408
|
-
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
409
|
-
yield { type: 'text-delta', index: 0, text }
|
|
410
|
-
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
|
|
414
|
-
function wireLlmStreams(ctx, configs, warnOnce, on) {
|
|
415
|
-
const disposers = []
|
|
416
|
-
for (const config of configs) {
|
|
417
|
-
disposers.push(on('llm/stream', (options, next, invocation) => {
|
|
418
|
-
try {
|
|
419
|
-
const frame = invocation ?? ruleFrame('llm/stream', [options], warnOnce)
|
|
420
|
-
const agent = options?.sessionId === undefined ? undefined : getService(ctx, 'agents')?.get?.(options.sessionId)
|
|
421
|
-
frame.subject.agent = agent
|
|
422
|
-
if (!ruleMatches(config.rule, frame)) return next()
|
|
423
|
-
const mode = config.params?.mode ?? 'pass'
|
|
424
|
-
if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
|
|
425
|
-
return replacedStream(config.texts.join('\n\n'))
|
|
426
|
-
}
|
|
427
|
-
return next()
|
|
428
|
-
} catch (error) {
|
|
429
|
-
warnOnce(`${name}: llm-stream config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
430
|
-
return next()
|
|
431
|
-
}
|
|
432
|
-
}))
|
|
433
|
-
}
|
|
434
|
-
return disposers
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
|
|
438
|
-
|
|
439
360
|
/**
|
|
440
361
|
* 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。
|
|
441
362
|
*
|
|
@@ -450,15 +371,17 @@ export function pluginMessage(prefix, text, summary) {
|
|
|
450
371
|
id: newMessageId(prefix),
|
|
451
372
|
role: 'user',
|
|
452
373
|
content: [{ type: 'text', text }],
|
|
453
|
-
source: { kind:
|
|
374
|
+
source: { kind: pluginKind(name), plugin: name, form: 'notice', summary },
|
|
454
375
|
}
|
|
455
376
|
}
|
|
456
377
|
|
|
457
378
|
/** 层级文本:按声明变量插值后按空行拼接;不走官方插值通道(非 system-section)。 */
|
|
458
379
|
function layerText(config, agent, warnOnce) {
|
|
459
380
|
try {
|
|
381
|
+
if (config.params?.templateEvaluation === true) return renderTemplate(config, { agent }, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce })
|
|
382
|
+
const variables = config[SESSION_VARIABLES_DISABLED] === true ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables
|
|
460
383
|
return config.texts
|
|
461
|
-
.map((item) => interpolateVariables(item,
|
|
384
|
+
.map((item, index) => interpolateVariables(item, variables, agent?.session, undefined, `${config.sourceModuleId}:${config.id}#${index}`))
|
|
462
385
|
.filter((item) => item.length > 0)
|
|
463
386
|
.join('\n\n')
|
|
464
387
|
} catch (error) {
|
|
@@ -467,56 +390,10 @@ function layerText(config, agent, warnOnce) {
|
|
|
467
390
|
}
|
|
468
391
|
}
|
|
469
392
|
|
|
470
|
-
/** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
|
|
471
|
-
function wireToolPipelines(ctx, configs, warnOnce, on) {
|
|
472
|
-
const disposers = []
|
|
473
|
-
for (const config of configs) {
|
|
474
|
-
const names = parseToolNames(config.params?.toolNames)
|
|
475
|
-
const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
|
|
476
|
-
disposers.push(on('tools/pre-execute', async (exec, next, invocation) => {
|
|
477
|
-
try {
|
|
478
|
-
if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/pre-execute', [exec], warnOnce))) return next()
|
|
479
|
-
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
480
|
-
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
|
|
481
|
-
const decision = config.params?.preDecision ?? 'allow'
|
|
482
|
-
if (decision === 'allow') return next()
|
|
483
|
-
if (decision === 'deny') {
|
|
484
|
-
return { kind: 'deny', reason: String(config.params?.denyReason ?? `${config.name}: denied by prompt config`) }
|
|
485
|
-
}
|
|
486
|
-
if (decision === 'ask') return { kind: 'ask' }
|
|
487
|
-
return next()
|
|
488
|
-
} catch (error) {
|
|
489
|
-
warnOnce(`${name}: tool-pipeline(pre) config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
490
|
-
return next()
|
|
491
|
-
}
|
|
492
|
-
}))
|
|
493
|
-
disposers.push(on('tools/post-execute', async (exec, result, next, invocation) => {
|
|
494
|
-
try {
|
|
495
|
-
if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/post-execute', [exec, result], warnOnce))) return next()
|
|
496
|
-
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
497
|
-
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
|
|
498
|
-
const action = config.params?.postAction ?? 'accept'
|
|
499
|
-
if (action === 'accept') return next()
|
|
500
|
-
if (action === 'replace' && config.texts.length > 0) {
|
|
501
|
-
return { kind: 'accept', content: [{ type: 'text', text: config.texts.join('\n\n') }] }
|
|
502
|
-
}
|
|
503
|
-
if (action === 'block') {
|
|
504
|
-
return { kind: 'block', feedback: [{ type: 'text', text: config.texts.length > 0 ? config.texts.join('\n\n') : `${config.name}: blocked by prompt config` }] }
|
|
505
|
-
}
|
|
506
|
-
return next()
|
|
507
|
-
} catch (error) {
|
|
508
|
-
warnOnce(`${name}: tool-pipeline(post) config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
509
|
-
return next()
|
|
510
|
-
}
|
|
511
|
-
}))
|
|
512
|
-
}
|
|
513
|
-
return disposers
|
|
514
|
-
}
|
|
515
|
-
|
|
516
393
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
394
|
+
* 轮次停止层(`turn-stop` 与续跑动作)的续跑上限:**每个来源(模块)**每轮 1 次、
|
|
395
|
+
* 每会话 3 次,均为引擎常量。不暴露为配置——强制续跑失控会把会话卡在停不下来的
|
|
396
|
+
* 循环里;官方桥在同一位置也只留了 TODO(stop-loop-guard) 而未实现上限。
|
|
520
397
|
*/
|
|
521
398
|
export const TURN_STOP_MAX_PER_TURN = 1
|
|
522
399
|
export const TURN_STOP_MAX_PER_SESSION = 3
|
|
@@ -525,8 +402,10 @@ export const TURN_STOP_MAX_PER_SESSION = 3
|
|
|
525
402
|
export const TURN_STOP_MAX_TRACKED_TURNS = 8
|
|
526
403
|
|
|
527
404
|
/**
|
|
528
|
-
*
|
|
405
|
+
* 续跑预算(唯一实现):所有 turn-stop 注入共用它,
|
|
529
406
|
* 上限是引擎常量而不是配置——强制续跑失控会把会话卡在停不下来的循环里。
|
|
407
|
+
* **一份预算覆盖一个来源模块**(规则运行时按模块传入同一份),因此同模块的多条
|
|
408
|
+
* turn-stop 动作合计每轮 1 次、每会话 3 次。
|
|
530
409
|
* 三步语义刻意与迁移前逐行一致:
|
|
531
410
|
* entry(...) 取条目(含创建与超限轮次淘汰的副作用),在条件判定**之前**调用;
|
|
532
411
|
* available(...) 只读判定,不落账;
|
|
@@ -541,9 +420,8 @@ export function createTurnStopBudget() {
|
|
|
541
420
|
* 不可从事件流重建的计数(与 `progress-reminder` 同档),丢失时机必须与迁移前逐字一致,
|
|
542
421
|
* compaction 也不重置它。
|
|
543
422
|
*
|
|
544
|
-
*
|
|
545
|
-
* `
|
|
546
|
-
* 本文件的 turn-stop 层)。`sessionState` 只在声明了 `reset` 时才读 ctx,因此这里传
|
|
423
|
+
* 本函数没有 ctx,只有 turn-stop 注入消费预算。
|
|
424
|
+
* `sessionState` 只在声明了 `reset` 时才读 ctx,因此这里传
|
|
547
425
|
* `undefined` 合法且零开销(详见 shared.mjs 的 JSDoc 与 test/engine/session-state.test.mjs)。
|
|
548
426
|
*/
|
|
549
427
|
const state = sessionState(undefined, () => ({ turns: new Map(), total: 0 }))
|
|
@@ -552,8 +430,7 @@ export function createTurnStopBudget() {
|
|
|
552
430
|
// 对外仍是 `(sessionId, turn)`:按 id 造一次性键宿主,`keyOf` 取的就是 `.id`,与旧的
|
|
553
431
|
// 记账写法写进同一个槽位、同一个 clear 时机。
|
|
554
432
|
// 唯一差异在**无 id 的会话**(`sessionId === undefined`):`sessionState` 刻意不记账
|
|
555
|
-
// (见 shared.mjs
|
|
556
|
-
// append-context 与下方 turn-stop 层)都在 `entry()` 之前守卫 `session?.id === undefined`,
|
|
433
|
+
// (见 shared.mjs),下方 turn-stop 层在 `entry()` 前守卫 `session?.id === undefined`,
|
|
557
434
|
// 故该路径不可达;这里兜底一个新条目,避免将来误用时抛 TypeError。
|
|
558
435
|
const entry = state.get({ id: sessionId }) ?? { turns: new Map(), total: 0 }
|
|
559
436
|
if (!Number.isFinite(turn)) return entry
|
|
@@ -578,11 +455,15 @@ export function createTurnStopBudget() {
|
|
|
578
455
|
}
|
|
579
456
|
}
|
|
580
457
|
|
|
581
|
-
/**
|
|
582
|
-
|
|
458
|
+
/**
|
|
459
|
+
* turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。
|
|
460
|
+
* options.turnStopBudgets 由调用方传入时按同一份预算记账(规则运行时按模块一份),
|
|
461
|
+
* 缺省仍按 sourceModuleId 各建一份。
|
|
462
|
+
*/
|
|
463
|
+
function wireTurnStops(ctx, configs, warnOnce, on, turnStopBudgets) {
|
|
583
464
|
const disposers = []
|
|
584
465
|
if (configs.length === 0) return disposers
|
|
585
|
-
const budgets = new Map()
|
|
466
|
+
const budgets = turnStopBudgets ?? new Map()
|
|
586
467
|
|
|
587
468
|
for (const config of configs) {
|
|
588
469
|
const source = config.sourceModuleId ?? ''
|
|
@@ -591,18 +472,20 @@ function wireTurnStops(ctx, configs, warnOnce, on) {
|
|
|
591
472
|
disposers.push(on('agent/turn-stopping', (payload = {}, _next, invocation) => {
|
|
592
473
|
const { agent, turn } = payload
|
|
593
474
|
try {
|
|
594
|
-
if (
|
|
475
|
+
if (payload.signal?.aborted) return
|
|
476
|
+
if (!actionMatches(config, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
|
|
595
477
|
const session = agent?.session
|
|
596
478
|
if (session?.id === undefined || typeof agent.steer !== 'function') return
|
|
597
479
|
if (!matchesAgentScope(config, agent)) return
|
|
480
|
+
if (config.dedupe === 'session' && (config.linkedDelivery ? findActionDelivery(session, config) : injectedMessage(config, session))) return
|
|
598
481
|
const entry = budget.entry(session.id, turn)
|
|
599
482
|
if (!budget.available(entry, turn)) return
|
|
600
|
-
if (!conditionHit(config, { assistantText: lastAssistantText(session) })) return
|
|
601
483
|
const text = layerText(config, agent, warnOnce)
|
|
602
484
|
if (text.length === 0) return
|
|
603
485
|
// 计数在 steer 之前落账:steer 抛错也不允许下一步重试越过预算。
|
|
604
486
|
budget.claim(entry, turn)
|
|
605
|
-
|
|
487
|
+
const message = pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`)
|
|
488
|
+
agent.steer({ ...message, source: { ...message.source, ...identityStampOf(config), ...actionStamp(config, session) } })
|
|
606
489
|
} catch (error) {
|
|
607
490
|
warnOnce(`${name}: turn-stop config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
608
491
|
}
|
|
@@ -644,7 +527,7 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
|
644
527
|
const session = child?.session ?? getService(ctx, 'sessions')?.get?.(info.id)
|
|
645
528
|
if (session === undefined) return
|
|
646
529
|
const mainId = mainSessionForChild(ctx, info.id)
|
|
647
|
-
//
|
|
530
|
+
// shortcut: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
|
|
648
531
|
if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
|
|
649
532
|
runs.set(info.runId, { id: info.id, mainId, agent: child, session, model: child?.options?.model })
|
|
650
533
|
}))
|
|
@@ -659,11 +542,9 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
|
659
542
|
if (child === undefined) return
|
|
660
543
|
const frame = invocation ?? ruleFrame('subagent/start', [info], warnOnce)
|
|
661
544
|
frame.subject.agent = child
|
|
662
|
-
const subagentText = subagentTextOf(info)
|
|
663
545
|
for (const config of startConfigs) {
|
|
664
|
-
if (!
|
|
546
|
+
if (!actionMatches(config, frame)) continue
|
|
665
547
|
if (!matchesAgentScope(config, child)) continue
|
|
666
|
-
if (!conditionHit(config, { subagentText })) continue
|
|
667
548
|
if (typeof child.inject !== 'function') continue
|
|
668
549
|
const text = layerText(config, child, warnOnce)
|
|
669
550
|
if (text.length === 0) continue
|
|
@@ -681,17 +562,16 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
|
681
562
|
const recorded = runs.get(info?.runId)
|
|
682
563
|
runs.delete(info?.runId)
|
|
683
564
|
if (recorded !== undefined && recorded.id !== info?.id) return
|
|
684
|
-
const subagentText = subagentTextOf(info)
|
|
685
565
|
const frame = invocation ?? ruleFrame('subagent/end', [info], warnOnce)
|
|
686
566
|
frame.subject.agent = child ?? recorded?.agent
|
|
687
567
|
frame.subject.session = child?.session ?? recorded?.session ?? getService(ctx, 'sessions')?.get?.(info?.id)
|
|
688
568
|
frame.subject.model = child?.options?.model ?? recorded?.model
|
|
689
569
|
for (const config of endConfigs) {
|
|
690
|
-
if (!
|
|
570
|
+
if (!actionMatches(config, frame)) continue
|
|
691
571
|
if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
|
|
692
|
-
if (!conditionHit(config, { subagentText })) continue
|
|
693
572
|
if (config.params?.action !== 'inject-main') {
|
|
694
|
-
|
|
573
|
+
// 模板本意是逐次记录;命中本身已由装配实例的判定账本记 hit,这里不占告警名额。
|
|
574
|
+
ctx.logger?.info?.(`${name}: subagent-end ${config.id} matched (observe only)`)
|
|
695
575
|
continue
|
|
696
576
|
}
|
|
697
577
|
if (typeof info?.runId !== 'string' || info.runId.length === 0) continue
|
|
@@ -707,7 +587,7 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
|
707
587
|
if (delivered === undefined) { delivered = new Map(); subagentEndDeliveries.set(main.session, delivered) }
|
|
708
588
|
const key = `${info.runId}:${config.id}`
|
|
709
589
|
if (delivered.has(key)) continue
|
|
710
|
-
//
|
|
590
|
+
// shortcut: 会话内保留最近的投递记录;超大历史改用持久事件索引时可取消此上限。
|
|
711
591
|
if (delivered.size >= MAX_TRACKED_SESSIONS) delivered.delete(delivered.keys().next().value)
|
|
712
592
|
delivered.set(key, true)
|
|
713
593
|
main.inject(pluginMessage(`subagent-end-${config.id}`, text, `subagent-end ${config.id}`))
|
|
@@ -720,43 +600,99 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
|
720
600
|
return disposers
|
|
721
601
|
}
|
|
722
602
|
|
|
603
|
+
/** 注册名与序号:字段只在注册点算一次,重排器只消费这里的结果。 */
|
|
604
|
+
function orderedEntry(name, config) {
|
|
605
|
+
return { name, sequence: config.sequence ?? config.order ?? 0, order: config.order ?? 0 }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* 实际注册名按 configOrder(sequence)排序;同序号保持注册序,同名只留一次。
|
|
610
|
+
* 名字来自注册点而不是事后从 configs 推导,避免 regular/conditional/placeholder 三条路径漂移。
|
|
611
|
+
*/
|
|
612
|
+
function orderedNamesOf(entries) {
|
|
613
|
+
// configOrder 优先;同序号回到官方 order(而不是接线顺序),最后 stable 保留注册顺序。
|
|
614
|
+
return [...new Set([...entries]
|
|
615
|
+
.sort((left, right) => left.sequence - right.sequence || (left.order ?? 0) - (right.order ?? 0))
|
|
616
|
+
.map((entry) => entry.name))]
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 官方先按 order/段名排好投递顺序;这里按 configOrder 把本插件注册的段/上下文
|
|
621
|
+
* 回填到它们原来的槽位。只换本插件段之间的位置:平台段与其他插件段占住的槽位不动。
|
|
622
|
+
*/
|
|
623
|
+
function registerDeliveryOrder(ctx, keep, collection, names) {
|
|
624
|
+
if (names.length < 2) return
|
|
625
|
+
const rank = new Map(names.map((entryName, index) => [entryName, index]))
|
|
626
|
+
keep(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
627
|
+
const result = await next()
|
|
628
|
+
const list = result?.[collection]
|
|
629
|
+
if (!Array.isArray(list)) return result
|
|
630
|
+
const slots = []
|
|
631
|
+
for (let index = 0; index < list.length; index++) {
|
|
632
|
+
if (rank.has(list[index]?.name)) slots.push(index)
|
|
633
|
+
}
|
|
634
|
+
if (slots.length < 2) return result
|
|
635
|
+
const ordered = slots.map((index) => list[index]).sort((left, right) => rank.get(left.name) - rank.get(right.name))
|
|
636
|
+
if (slots.every((slot, index) => list[slot] === ordered[index])) return result
|
|
637
|
+
const nextList = [...list]
|
|
638
|
+
slots.forEach((slot, index) => { nextList[slot] = ordered[index] })
|
|
639
|
+
return { ...result, [collection]: nextList }
|
|
640
|
+
}, { prepend: true }), `${name}: ${collection} order`)
|
|
641
|
+
}
|
|
642
|
+
|
|
723
643
|
/**
|
|
724
644
|
* 把非 pre-step 提示词配置接入其声明的官方层级通道。
|
|
725
|
-
* @
|
|
726
|
-
*
|
|
645
|
+
* @param options.agent 当前 ctx 所属的真实 Agent;固定效果的受众在注册前匹配。
|
|
646
|
+
* @param options.turnStopBudgets 可选的续跑预算映射(键 = 模块 id,来源缺失用空串兜底);
|
|
647
|
+
* 规则运行时按模块传入同一份,使同模块的 turn-stop 动作共享上限。
|
|
648
|
+
* @returns 聚合 disposer:回收监听器及段/上下文/变量注册;后者也绑定 ctx fiber,重复释放幂等。
|
|
727
649
|
*/
|
|
728
650
|
export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
651
|
+
// 固定效果必须在目标 Agent 注册前筛选;空正文不能撤销官方 complete/suppress 声明。
|
|
652
|
+
configs = configs.filter(config => {
|
|
653
|
+
if (config.layer !== 'system-section' || config.audience == null
|
|
654
|
+
|| config.params?.complete !== true && config.params?.suppressRuntimeContext !== true) return true
|
|
655
|
+
if (options.agent?.session === undefined) throw new TypeError(`${name}: fixed system-section ${config.id} with audience requires an explicit Agent in options.agent`)
|
|
656
|
+
return matchesAudience(config.audience, isDelegated(options.agent.session) ? 'subagent' : 'main')
|
|
657
|
+
})
|
|
729
658
|
configs = [...configs].sort(compareConfigSequence)
|
|
659
|
+
// 没有注入通道的层(`tool-pipeline`)在这里没有分派:显式告警一次,别让 config 级入口静默丢弃。
|
|
660
|
+
for (const config of configs) {
|
|
661
|
+
if (!INJECTION_LAYERS.includes(config.layer)) warnOnce(`${name}: ${config.id}: layer ${String(config.layer)} 没有注入通道,已跳过(工具链的裁决请用 decision / append-context)`)
|
|
662
|
+
}
|
|
730
663
|
const owned = []
|
|
731
664
|
const keep = (dispose, label) => {
|
|
732
665
|
if (typeof dispose !== 'function') return
|
|
733
666
|
let active = true
|
|
734
667
|
const release = () => { if (active) { active = false; dispose() } }
|
|
735
668
|
owned.push(release); keepDisposer(ctx, release, label)
|
|
669
|
+
return release
|
|
736
670
|
}
|
|
737
|
-
const on = options.on ?? ((...
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// 释放失败不得反过来打断卸载流程。
|
|
759
|
-
}
|
|
671
|
+
const on = (...args) => keep((options.on ?? ((...values) => ctx.on(...values)))(...args), `${name}: ${args[0]}`)
|
|
672
|
+
const dispose = () => {
|
|
673
|
+
for (const release of owned.splice(0).reverse()) { try { release() } catch {} }
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
// 注册即纳入 owned,后续注册失败也能释放已取得的变量和监听器。
|
|
677
|
+
/** 实际注册名与序号:重排器只认这里,不从 configs 反推。 */
|
|
678
|
+
const ordered = { sections: [], contexts: [] }
|
|
679
|
+
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep)
|
|
680
|
+
const conditional = configs.filter(config => ['system-section', 'runtime-context'].includes(config.layer)
|
|
681
|
+
&& (typeof config.rule?.when === 'function' || typeof config.actionWhen === 'function'
|
|
682
|
+
|| (config.rule !== undefined || config.params?.templateEvaluation === true) && config.params?.complete !== true && config.params?.suppressRuntimeContext !== true))
|
|
683
|
+
const regular = configs.filter(config => !conditional.includes(config))
|
|
684
|
+
wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep, ordered)
|
|
685
|
+
wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on, ordered)
|
|
686
|
+
wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on, ordered)
|
|
687
|
+
wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on, options.turnStopBudgets)
|
|
688
|
+
wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on)
|
|
689
|
+
// 官方 order 只提供初始槽位;configOrder 覆盖段/上下文的先后(平台段与其他插件段不动)。
|
|
690
|
+
for (const collection of ['sections', 'contexts']) {
|
|
691
|
+
registerDeliveryOrder(ctx, keep, collection, orderedNamesOf(ordered[collection]))
|
|
760
692
|
}
|
|
693
|
+
return dispose
|
|
694
|
+
} catch (error) {
|
|
695
|
+
dispose()
|
|
696
|
+
throw error
|
|
761
697
|
}
|
|
762
698
|
}
|