dsh-plugin-prompt-tool 0.7.2 → 0.8.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 +185 -46
- package/engine/THIRD_PARTY_LICENSES +25 -0
- package/engine/actions.mjs +931 -0
- package/engine/anchor-match.mjs +6 -3
- package/engine/character-tools.mjs +11 -0
- package/engine/classify-task.mjs +18 -0
- package/engine/compaction-epoch.mjs +41 -20
- package/engine/compositions/library/agent-instructions.yml +9 -0
- package/engine/compositions/library/command-goal.yml +2 -6
- package/engine/compositions/library/compaction.yml +2 -32
- package/engine/compositions/library/delegation-ptc.yml +58 -0
- package/engine/compositions/library/delegation.yml +5 -38
- package/engine/compositions/library/persistent-shell.yml +4 -13
- package/engine/compositions/library/planning.yml +8 -23
- package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/skill-filesystem.yml +2 -8
- package/engine/compositions/library/tool-ask-user.yml +2 -3
- package/engine/compositions/library/tool-bash.yml +4 -12
- package/engine/compositions/library/tool-cordis.yml +7 -0
- package/engine/compositions/library/tool-fs-search.yml +2 -11
- package/engine/compositions/library/tool-fs.yml +2 -5
- package/engine/compositions/library/tool-goal.yml +2 -6
- package/engine/compositions/library/tool-jobs.yml +2 -18
- package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
- package/engine/compositions/library/tool-plugin-manager.yml +8 -0
- package/engine/compositions/library/tool-present.yml +7 -0
- package/engine/compositions/library/tool-presentation.yml +9 -0
- package/engine/compositions/library/tool-pwsh.yml +2 -6
- package/engine/compositions/library/tool-skill.yml +7 -0
- package/engine/compositions/library/tool-todo.yml +2 -4
- package/engine/compositions/library/tool-web.yml +2 -1
- package/engine/compositions/source/local/character-tools.yml +5 -0
- package/engine/compositions/source/local/declared-triggers.yml +12 -0
- package/engine/compositions/source/local/filesystem-editor.yml +30 -0
- package/engine/compositions/source/local/instruction-hint.yml +15 -0
- package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
- package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
- package/engine/compositions/source/local/run-code-env.yml +4 -7
- package/engine/compositions/source/local/session-var-tools.yml +5 -0
- package/engine/compositions/source/local/skill-search.yml +3 -9
- package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
- package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
- package/engine/compositions/source/local/tool-config-engine.yml +8 -0
- package/engine/compositions/source/local/tool-git-bash.yml +15 -0
- package/engine/compositions/source/local/world-book-tools.yml +5 -0
- package/engine/condition.mjs +83 -0
- package/engine/declared-triggers.mjs +93 -0
- package/engine/executor.mjs +364 -185
- package/engine/fields.mjs +219 -0
- package/engine/fillers.mjs +12 -141
- package/engine/host-package.mjs +51 -0
- package/engine/instruction-hint.mjs +285 -0
- package/engine/interpolate.mjs +157 -29
- package/engine/layers.mjs +495 -75
- package/engine/predicates.mjs +753 -0
- package/engine/prompt-config-engine.mjs +63 -8
- package/engine/run-code-env.mjs +48 -31
- package/engine/schema.mjs +444 -103
- package/engine/sdk-strip.mjs +365 -0
- package/engine/session-var-tools.mjs +11 -0
- package/engine/session-vars.mjs +8 -6
- package/engine/shared.mjs +137 -0
- package/engine/skill-search.mjs +34 -21
- package/engine/st-macros.d.mts +22 -0
- package/engine/st-macros.mjs +208 -0
- package/engine/st-render.mjs +105 -0
- package/engine/st-world-book.d.mts +10 -0
- package/engine/st-world-book.mjs +259 -0
- package/engine/strategies.mjs +31 -20
- package/engine/subagent-tool-policy-core.mjs +400 -0
- package/engine/subagent-tool-policy.mjs +327 -0
- package/engine/tool-config-engine.mjs +207 -145
- package/engine/tool-definition.mjs +143 -0
- package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
- package/engine/trigger-editor-meta.mjs +48 -0
- package/engine/trigger-spec.mjs +257 -0
- package/engine/trigger.mjs +304 -0
- package/engine/world-book-tools.mjs +11 -0
- package/lib/client.js +16495 -5456
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +628 -382
- package/lib/index.mjs +5391 -2942
- package/lib/paths-CY5psBqE.d.mts +1233 -0
- package/lib/preset-package-BnJqrU4R.mjs +16792 -0
- package/lib/preset-transfer.d.mts +2 -0
- package/lib/preset-transfer.mjs +2 -0
- package/package.json +91 -49
- package/preset/pt-cordis/preset.yml +43 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
- package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
- package/preset/pt-custom/preset.yml +15 -0
- package/preset/pt-minimal/preset.yml +27 -0
- package/preset/pt-ptc/preset.yml +45 -0
- package/preset/pt-standard/preset.yml +43 -0
- package/templates/14-first-turn-anchor.yml +4 -4
- package/templates/15-guide-auto.yml +3 -3
- package/templates/16-custom-fallback.yml +1 -0
- package/templates/18-placeholder-env-facts.yml +3 -3
- package/templates/20-system-section.yml +3 -2
- package/templates/65-turn-stop.yml +18 -0
- package/templates/66-subagent-start.yml +14 -0
- package/templates/75-subagent-end.yml +15 -0
- package/templates/tools/10-shell-command.yml +1 -1
- package/templates/tools/40-delegate-character.yml +1 -1
- package/templates/tools/50-delegate-worldbook.yml +1 -1
- package/templates/tools/60-delegate-session-var.yml +1 -1
- package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
- package/engine/anchor-turn.mjs +0 -68
- package/engine/code-presentation.mjs +0 -103
- package/engine/compositions/library/anchor-turn.yml +0 -10
- package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
- package/engine/compositions/library/code-presentation.yml +0 -8
- package/engine/compositions/library/context-gate.yml +0 -39
- package/engine/compositions/library/cot-drip.yml +0 -11
- package/engine/compositions/library/custom-bash.yml +0 -14
- package/engine/compositions/library/deliberation-gate.yml +0 -11
- package/engine/compositions/library/official-agent-instructions.yml +0 -17
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
- package/engine/compositions/library/official-tool-bash.yml +0 -6
- package/engine/compositions/library/official-tool-cordis.yml +0 -13
- package/engine/compositions/library/official-tool-presentation.yml +0 -7
- package/engine/compositions/library/official-tool-skill.yml +0 -14
- package/engine/compositions/library/persona.yml +0 -13
- package/engine/compositions/library/prompt-config-engine.yml +0 -16
- package/engine/compositions/library/run-code-env.yml +0 -15
- package/engine/compositions/library/skill-search.yml +0 -14
- package/engine/compositions/library/str-replace-editor.yml +0 -10
- package/engine/compositions/library/tool-bootstrap.yml +0 -14
- package/engine/compositions/library/tool-config-engine.yml +0 -9
- package/engine/compositions/library/tool-filter.yml +0 -8
- package/engine/compositions/source/local/code-presentation.yml +0 -8
- package/engine/compositions/source/local/context-gate.yml +0 -36
- package/engine/compositions/source/local/custom-bash.yml +0 -11
- package/engine/compositions/source/local/str-replace-editor.yml +0 -10
- package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
- package/engine/compositions/source/local/tool-filter.yml +0 -8
- package/engine/context-gate.mjs +0 -305
- package/engine/cot-drip.mjs +0 -137
- package/engine/deliberation-gate.mjs +0 -127
- package/engine/tool-bootstrap.mjs +0 -476
- package/engine/tool-filter.mjs +0 -80
- package/lib/preset-core.d.mts +0 -10
- package/lib/preset-core.mjs +0 -77
- package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
- package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
- package/preset/anchored/preset.yml +0 -296
- package/preset/creative/preset.yml +0 -67
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
- package/preset/custom/preset.yml +0 -12
- package/preset/liangshen/preset.yml +0 -75
- package/preset/minimal/preset.yml +0 -45
- package/preset/ptc/preset.yml +0 -58
- package/preset/standard/preset.yml +0 -57
- package/skills/manifest.json +0 -7
- package/skills/sandboxmod/SKILL.md +0 -49
- package/skills/web ui/SKILL.md +0 -42
package/engine/layers.mjs
CHANGED
|
@@ -4,26 +4,140 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
MAX_TRACKED_SESSIONS,
|
|
8
|
+
extractText,
|
|
7
9
|
getService,
|
|
8
10
|
isDelegated,
|
|
9
11
|
keepDisposer,
|
|
10
12
|
matchesModel,
|
|
13
|
+
newMessageId,
|
|
11
14
|
parseToolNames,
|
|
15
|
+
sessionState,
|
|
12
16
|
} from './shared.mjs'
|
|
13
|
-
import {
|
|
17
|
+
import { KNOWN_STRATEGIES } from './schema.mjs'
|
|
18
|
+
import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue } from './interpolate.mjs'
|
|
19
|
+
import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
|
|
20
|
+
import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
|
|
14
21
|
|
|
15
22
|
const name = 'prompt-config-engine'
|
|
23
|
+
// 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
|
|
24
|
+
// B4 保留原状(不迁 `sessionState`):外层确实是「按会话索引」的 WeakMap,但它存的不是
|
|
25
|
+
// 会话条目而是**第二层索引**——内层 `Map` 按 `${runId}:${config.id}` 去重,那是运行级的键,
|
|
26
|
+
// 与统一接口的「按会话索引」不同层;套上去只剩一个改了名字的 WeakMap。内层的
|
|
27
|
+
// `delete(最旧)` 也是与 `clear()` 不同的另一档淘汰策略(见下方 `runs` 的保留理由)。
|
|
28
|
+
const subagentEndDeliveries = new WeakMap()
|
|
16
29
|
|
|
17
|
-
/** 单条文本型配置的完整文本:texts 数组按空行拼接(单一文本字段)。
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
/** 单条文本型配置的完整文本:texts 数组按空行拼接(单一文本字段)。
|
|
31
|
+
* keep=官方变量通道按 assembly 求值的名字,静态 pass 命中即原样保留引用。 */
|
|
32
|
+
function configText(config, registries, warnOnce, context) {
|
|
33
|
+
const registry = registries.get(config)
|
|
34
|
+
if (typeof config.renderSt === 'function') {
|
|
35
|
+
return officialChannelText(config.renderSt(context?.agent, [], warnOnce, context), `${config.layer} ${config.id}`, registry, warnOnce)
|
|
36
|
+
}
|
|
37
|
+
const text = config.texts
|
|
38
|
+
.map((item) => interpolateVariables(item, config.variables, context?.agent?.session, registry?.protect))
|
|
21
39
|
.filter((item) => item.length > 0)
|
|
22
40
|
.join('\n\n')
|
|
41
|
+
return officialChannelText(text, `${config.layer} ${config.id}`, registry, warnOnce)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 官方 prompt 变量名规则(与官方 system-prompt 的 VARIABLE_NAME 一致)。 */
|
|
45
|
+
const OFFICIAL_NAME_RE = /^[a-z][a-z0-9_]*$/
|
|
46
|
+
/** 引用形态(仅名字,无 `::` 参数):事实与内容变量引用都用这一形态。 */
|
|
47
|
+
const NAME_REFERENCE_RE = /\{\{\s*([A-Za-z0-9_.\u4e00-\u9fff-]+)\s*\}\}/g
|
|
48
|
+
|
|
49
|
+
/** 名字哈希(djb2 → base36):非法官方名改写的确定性后缀。 */
|
|
50
|
+
function shortHash(text) {
|
|
51
|
+
let hash = 5381
|
|
52
|
+
for (let index = 0; index < text.length; index += 1) hash = ((hash * 33) ^ text.charCodeAt(index)) >>> 0
|
|
53
|
+
return hash.toString(36)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 非法官方名的确定性改写:sv_<ascii slug>_<hash>;slug 为空(纯中文名)时只用 hash。 */
|
|
57
|
+
function officialAliasOf(name) {
|
|
58
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
59
|
+
return `sv_${slug.length > 0 ? `${slug}_` : ''}${shortHash(name)}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 注册官方 prompt 变量(systemPrompt.variable,按 assembly 求值):
|
|
64
|
+
* - 运行时事实(ST 运行时宏 + 时间类):值随每次 assembly 现算,不再在注册期冻结为空;
|
|
65
|
+
* - 官方通道文本里被引用、且已声明的内容变量:值 = 会话变量覆盖 ?? 声明值。
|
|
66
|
+
* 非法官方名(中文/大写/连字符)改写成确定性别名并改写文本引用;注册失败的名字退回静态解析。
|
|
67
|
+
* @returns 各配置独立的引用白名单与别名;同一作用域内等价绑定只注册一次。
|
|
68
|
+
*/
|
|
69
|
+
function registerOfficialVariables(ctx, configs, warnOnce) {
|
|
70
|
+
const registries = new Map(configs.map((config) => [config, { protect: new Set(), alias: new Map(), registered: new Set() }]))
|
|
71
|
+
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
72
|
+
if (systemPrompt === undefined || typeof systemPrompt.variable !== 'function') return registries
|
|
73
|
+
const references = new Map()
|
|
74
|
+
const reserved = new Set()
|
|
75
|
+
const facts = new Set()
|
|
76
|
+
for (const config of configs) {
|
|
77
|
+
if (config.params?.stMacros === true) continue
|
|
78
|
+
const referenced = new Set()
|
|
79
|
+
for (const text of config.texts) {
|
|
80
|
+
for (const match of String(text).matchAll(NAME_REFERENCE_RE)) referenced.add(match[1])
|
|
81
|
+
}
|
|
82
|
+
references.set(config, referenced)
|
|
83
|
+
for (const name of referenced) {
|
|
84
|
+
if (!Object.hasOwn(config.variables, name) && RUNTIME_FACTS.has(name.toLowerCase())) facts.add(name.toLowerCase())
|
|
85
|
+
if (OFFICIAL_NAME_RE.test(name)) reserved.add(name)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const fact of facts) reserved.add(fact)
|
|
89
|
+
const bindings = new Map()
|
|
90
|
+
const usedNames = new Set()
|
|
91
|
+
for (const [config, referenced] of references) {
|
|
92
|
+
const registry = registries.get(config)
|
|
93
|
+
for (const name of referenced) {
|
|
94
|
+
const declared = Object.hasOwn(config.variables, name)
|
|
95
|
+
const isFact = !declared && RUNTIME_FACTS.has(name.toLowerCase())
|
|
96
|
+
if (!declared && !isFact) continue
|
|
97
|
+
let official = isFact ? name.toLowerCase() : OFFICIAL_NAME_RE.test(name) ? name : officialAliasOf(name)
|
|
98
|
+
try {
|
|
99
|
+
// 内容绑定包含局部命名空间:会话覆盖也可以再引用其中的变量。
|
|
100
|
+
// 纯事实不属于任何配置的内容命名空间,所有大小写变体复用规范名。
|
|
101
|
+
const signature = isFact ? `fact:${official}` : JSON.stringify([name, Object.entries(config.variables).sort(([a], [b]) => a.localeCompare(b))])
|
|
102
|
+
let binding = bindings.get(signature)
|
|
103
|
+
if (binding === undefined) {
|
|
104
|
+
if (usedNames.has(official) || (!isFact && facts.has(official)) || (official !== name && !isFact && reserved.has(official))) {
|
|
105
|
+
const base = officialAliasOf(`${name}:${signature}`)
|
|
106
|
+
official = base
|
|
107
|
+
for (let suffix = 1; usedNames.has(official) || reserved.has(official); suffix++) official = `${base}_${suffix}`
|
|
108
|
+
}
|
|
109
|
+
usedNames.add(official)
|
|
110
|
+
const source = isFact ? name.toLowerCase() : name
|
|
111
|
+
const declaredValue = declared ? String(config.variables[name] ?? '') : undefined
|
|
112
|
+
keepDisposer(ctx, systemPrompt.variable(official, (context) => {
|
|
113
|
+
const session = context?.agent?.session
|
|
114
|
+
const override = getSessionVar(session, source)
|
|
115
|
+
const value = override !== undefined ? String(override) : declaredValue ?? runtimeFactValue(source, session) ?? ''
|
|
116
|
+
const expanded = interpolateVariables(value, { ...config.variables, ...sessionVarsSnapshot(session) }, session)
|
|
117
|
+
return officialChannelText(expanded, `variable ${official}`, { alias: new Map(), registered: new Set() }, warnOnce)
|
|
118
|
+
}), `${name}: official prompt variable ${official}`)
|
|
119
|
+
binding = official
|
|
120
|
+
bindings.set(signature, binding)
|
|
121
|
+
}
|
|
122
|
+
registry.registered.add(binding)
|
|
123
|
+
registry.protect.add(name)
|
|
124
|
+
if (binding !== name) registry.alias.set(name, binding)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
warnOnce(`${name}: 官方 prompt 变量 ${official} 注册失败,该引用退回静态解析:${String(error?.message ?? error)}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return registries
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 非法名的引用改写:`{{中文名}}` → `{{sv_中文_<hash>}}`(仅已注册成功的别名)。 */
|
|
134
|
+
function rewriteOfficialAliases(text, alias) {
|
|
135
|
+
if (alias.size === 0) return text
|
|
136
|
+
return text.replace(NAME_REFERENCE_RE, (whole, name) => alias.has(name) ? `{{${alias.get(name)}}}` : whole)
|
|
23
137
|
}
|
|
24
138
|
|
|
25
139
|
/** agent/request 与 tools/* 层的作用域过滤。 */
|
|
26
|
-
function matchesAgentScope(config, agent) {
|
|
140
|
+
export function matchesAgentScope(config, agent) {
|
|
27
141
|
if (agent === undefined) return true
|
|
28
142
|
const delegated = isDelegated(agent.session)
|
|
29
143
|
if (config.audience === 'main' && delegated) return false
|
|
@@ -31,6 +145,21 @@ function matchesAgentScope(config, agent) {
|
|
|
31
145
|
return matchesModel(config.modelScope, agent.options?.model)
|
|
32
146
|
}
|
|
33
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 官方插值通道(system-section / runtime-context)出口清洗:本项目解析后仍残留的
|
|
150
|
+
* 引用一律剥离,官方严格插值因此看不到未注册引用(既不抛错也不留字面)。
|
|
151
|
+
* 只在本项目的宽容解析之后调用;其他层(pre-step / agent-request / tool-pipeline)
|
|
152
|
+
* 不经官方插值,保持原有宽容语义。
|
|
153
|
+
*/
|
|
154
|
+
function officialChannelText(text, label, registry, warnOnce) {
|
|
155
|
+
const result = stripUnresolvedRefs(rewriteOfficialAliases(text, registry.alias), registry.registered)
|
|
156
|
+
if (result.stripped.length > 0) {
|
|
157
|
+
const samples = [...new Set(result.stripped)].slice(0, 3).join(' ')
|
|
158
|
+
warnOnce(`${name}: ${label} 剥离了 ${result.stripped.length} 处无法解析的引用(${samples})——请在模板变量里登记,或改用本项目宏语法 {{roll::1d6}}`)
|
|
159
|
+
}
|
|
160
|
+
return result.text
|
|
161
|
+
}
|
|
162
|
+
|
|
34
163
|
/** 文本型层分组:merged 模式按位置分组,否则每条独立。 */
|
|
35
164
|
function textLayerGroups(configs) {
|
|
36
165
|
const sorted = [...configs].sort((a, b) => a.order - b.order)
|
|
@@ -53,51 +182,37 @@ function textLayerGroups(configs) {
|
|
|
53
182
|
return groups
|
|
54
183
|
}
|
|
55
184
|
|
|
56
|
-
/** persona 段标记(官方 deployment:persona + 旧名 persona)。 */
|
|
57
|
-
function isPersonaSection(config) {
|
|
58
|
-
const sectionName = config?.params?.sectionName
|
|
59
|
-
return sectionName === 'deployment:persona' || sectionName === 'persona'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
185
|
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
63
|
-
function wireSystemSections(ctx, configs, warnOnce) {
|
|
186
|
+
function wireSystemSections(ctx, configs, registry, warnOnce) {
|
|
187
|
+
const disposers = []
|
|
64
188
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
65
189
|
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
66
190
|
if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — system-section configs skipped`)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
const
|
|
72
|
-
.filter((config) => config.audience === 'subagent' && isPersonaSection(config))
|
|
73
|
-
.sort((a, b) => a.order - b.order)[0]
|
|
74
|
-
const registerable = configs.filter((config) => !(config.audience === 'subagent' && isPersonaSection(config)))
|
|
75
|
-
for (const group of textLayerGroups(registerable)) {
|
|
191
|
+
return disposers
|
|
192
|
+
}
|
|
193
|
+
// 人设段(deployment:persona-prefix/suffix)由官方 @deepseek-ai/dsh-persona 行注册
|
|
194
|
+
// (preset.yml 顶层 persona 段驱动);本层只处理其余 system-section 配置。
|
|
195
|
+
for (const group of textLayerGroups(configs)) {
|
|
76
196
|
const base = group[0]
|
|
77
197
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const text = isPersonaMain && subPersona !== undefined
|
|
85
|
-
? (context) => isDelegated(context?.agent?.session) ? configText(subPersona) : groupText
|
|
86
|
-
: hasAudience
|
|
87
|
-
? (context) => group
|
|
198
|
+
const dynamic = group.some((config) => config.audience != null || typeof config.renderSt === 'function'
|
|
199
|
+
|| config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
|
|
200
|
+
const groupText = dynamic ? '' : group.map((config) => configText(config, registry, warnOnce)).filter((item) => item.length > 0).join('\n\n')
|
|
201
|
+
if (!dynamic && groupText.length === 0) continue
|
|
202
|
+
const text = dynamic
|
|
203
|
+
? (context) => group
|
|
88
204
|
.filter((config) => matchesAgentScope(config, context?.agent))
|
|
89
|
-
.map((config) => configText(config))
|
|
205
|
+
.map((config) => configText(config, registry, warnOnce, context))
|
|
90
206
|
.filter((item) => item.length > 0)
|
|
91
207
|
.join('\n\n')
|
|
92
|
-
|
|
208
|
+
: groupText
|
|
93
209
|
keepDisposer(ctx, systemPrompt.section({
|
|
94
210
|
name: typeof base.params?.sectionName === 'string' && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
|
|
95
211
|
order: base.order,
|
|
96
212
|
text,
|
|
97
213
|
...(base.params?.complete === true ? { complete: true } : {}),
|
|
98
214
|
}), `${name}: section ${base.id}`)
|
|
99
|
-
//
|
|
100
|
-
// 快照(官方 dsh-persona includeRuntimeContext:false 等价;多段重复调用幂等)。
|
|
215
|
+
// suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
|
|
101
216
|
if (base.params?.suppressRuntimeContext === true) {
|
|
102
217
|
keepDisposer(ctx, systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
|
|
103
218
|
}
|
|
@@ -105,21 +220,29 @@ function wireSystemSections(ctx, configs, warnOnce) {
|
|
|
105
220
|
warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
106
221
|
}
|
|
107
222
|
}
|
|
223
|
+
return disposers
|
|
108
224
|
}
|
|
109
225
|
|
|
110
|
-
/** runtime-context
|
|
111
|
-
function wireRuntimeContexts(ctx, configs, warnOnce) {
|
|
226
|
+
/** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
|
|
227
|
+
function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
|
|
228
|
+
const disposers = []
|
|
112
229
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
113
230
|
if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
|
|
114
231
|
if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — runtime-context configs skipped`)
|
|
115
|
-
return
|
|
232
|
+
return disposers
|
|
116
233
|
}
|
|
117
|
-
|
|
234
|
+
// 模板专属策略(strategyDir 懒加载)与 placeholder 一样由 waterfall 按 assembly 消费
|
|
235
|
+
// resolve——`config.resolve` 只在这两层被调用,按静态注册会让「配了没效果也不报错」。
|
|
236
|
+
const needsResolver = (config) => config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy)
|
|
237
|
+
const staticConfigs = configs.filter((config) => !needsResolver(config))
|
|
118
238
|
for (const group of textLayerGroups(staticConfigs)) {
|
|
119
239
|
const base = group[0]
|
|
120
240
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
241
|
+
const dynamic = group.some(config => typeof config.renderSt === 'function'
|
|
242
|
+
|| config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
|
|
243
|
+
const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
|
|
244
|
+
const text = dynamic ? render : render()
|
|
245
|
+
if (!dynamic && text.length === 0) continue
|
|
123
246
|
keepDisposer(ctx, systemPrompt.context({
|
|
124
247
|
name: typeof base.params?.contextName === 'string' && base.params.contextName.length > 0 ? base.params.contextName : base.id,
|
|
125
248
|
order: base.order,
|
|
@@ -129,49 +252,105 @@ function wireRuntimeContexts(ctx, configs, warnOnce) {
|
|
|
129
252
|
warnOnce(`${name}: runtime-context config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
130
253
|
}
|
|
131
254
|
}
|
|
132
|
-
//
|
|
133
|
-
|
|
255
|
+
// 官方 provider 必须同步:先注册可渲染为空的私有变量占位,再在 waterfall 中填充。
|
|
256
|
+
// 标记随官方排序/遮蔽进入本次 assembly,无会话缓存,也不依赖调用方 context 的对象身份。
|
|
257
|
+
const placeholders = configs.filter(needsResolver)
|
|
134
258
|
.sort((a, b) => a.order - b.order)
|
|
259
|
+
if (placeholders.length === 0) return disposers
|
|
260
|
+
const slotVariable = `pt_runtime_${newMessageId('context').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
|
|
261
|
+
const slotText = `{{${slotVariable}}}`
|
|
262
|
+
const registered = new Map()
|
|
263
|
+
let active = true
|
|
264
|
+
keepDisposer(ctx, () => { active = false }, `${name}: runtime-context lifecycle`)
|
|
265
|
+
keepDisposer(ctx, systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
|
|
135
266
|
for (const config of placeholders) {
|
|
136
267
|
try {
|
|
137
|
-
const
|
|
268
|
+
const contextName = typeof config.params?.contextName === 'string' && config.params.contextName.length > 0 ? config.params.contextName : config.id
|
|
138
269
|
keepDisposer(ctx, systemPrompt.context({
|
|
139
|
-
name:
|
|
270
|
+
name: contextName,
|
|
140
271
|
order: config.order,
|
|
141
|
-
text:
|
|
142
|
-
const agent = assembly?.agent
|
|
143
|
-
const session = agent?.session
|
|
144
|
-
const resolved = await resolver({ ctx, agent, session, decision: { kind: 'ok', messages: [] }, messages: [] })
|
|
145
|
-
if (resolved === null || resolved === undefined) return ''
|
|
146
|
-
const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
147
|
-
if (config.texts.length > 0) return interpolateVariables(config.texts.join('\n\n'), variables, session)
|
|
148
|
-
return typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
|
|
149
|
-
},
|
|
272
|
+
text: slotText,
|
|
150
273
|
}), `${name}: context ${config.id}`)
|
|
274
|
+
registered.set(contextName, config)
|
|
151
275
|
} catch (error) {
|
|
152
276
|
warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
153
277
|
}
|
|
154
278
|
}
|
|
279
|
+
disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
280
|
+
const entries = assembly.contexts.filter(entry => registered.has(entry.name) && entry.text === slotText)
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const config = registered.get(entry.name)
|
|
283
|
+
entry.text = ''
|
|
284
|
+
if (!active || context.signal?.aborted) continue
|
|
285
|
+
try {
|
|
286
|
+
const agent = context.agent
|
|
287
|
+
const session = agent?.session
|
|
288
|
+
const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
|
|
289
|
+
if (!active || context.signal?.aborted || resolved === null || resolved === undefined) continue
|
|
290
|
+
const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
291
|
+
const rendered = config.texts.length > 0
|
|
292
|
+
? interpolateVariables(config.texts.join('\n\n'), variables, session)
|
|
293
|
+
: typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
|
|
294
|
+
entry.text = officialChannelText(rendered, `runtime-context ${config.id}`, registry.get(config), warnOnce)
|
|
295
|
+
} catch (error) {
|
|
296
|
+
warnOnce(`${name}: runtime-context ${config.id} resolve failed: ${String(error?.message ?? error)}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!active || context.signal?.aborted) for (const entry of entries) entry.text = ''
|
|
300
|
+
return next()
|
|
301
|
+
}))
|
|
302
|
+
return disposers
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* agent/request 载荷的唯一改写实现(B3 T2 第 (7) 类动作与既有 `agent-request`
|
|
307
|
+
* 层**共用这一份**,不另写一套请求改写)。三种语义:
|
|
308
|
+
* - `patch`:对下游已解析的 LlmCallConfig 做浅合并;
|
|
309
|
+
* - `replace: true`:整体替换为 `patch`(不叠加 base,故 `unset` 无比较对象);
|
|
310
|
+
* - `unset`:**按值条件删键** —— 仅当合并结果里的值**恰好等于**声明值时删除该键
|
|
311
|
+
* (`Object.is` 比较)。这来自原 `tool-bootstrap` 剥离 `bootstrapMaxTokens` 的语义
|
|
312
|
+
* (「仅当解析后的 maxTokens 恰好等于本声明注入的那个值时才删」;声明侧见
|
|
313
|
+
* `test/engine/declarations/tool-bootstrap.yml` 声明 4 的 `do.unset`),不是"存在即删"
|
|
314
|
+
* ——模型或其它插件设置的同名参数不会被删。
|
|
315
|
+
* 浅合并做不到删键,故删键必须走显式通道。
|
|
316
|
+
* @param params 提示词配置的 `params`(或动作的等价声明)。
|
|
317
|
+
* @param base 下游 `next()` 已解析的请求配置。
|
|
318
|
+
* @returns 改写后的请求配置;无 unset 命中时零额外分配。
|
|
319
|
+
*/
|
|
320
|
+
export function applyAgentRequestParams(params, base) {
|
|
321
|
+
const patch = params?.patch !== null && typeof params?.patch === 'object' && !Array.isArray(params.patch)
|
|
322
|
+
? params.patch
|
|
323
|
+
: {}
|
|
324
|
+
if (params?.replace === true) return { ...patch }
|
|
325
|
+
const merged = { ...base, ...patch }
|
|
326
|
+
const unset = params?.unset
|
|
327
|
+
if (unset === null || typeof unset !== 'object' || Array.isArray(unset)) return merged
|
|
328
|
+
let result
|
|
329
|
+
for (const [key, expected] of Object.entries(unset)) {
|
|
330
|
+
// 每个键都对着**合并结果**判定:一旦分配过副本,后续键不得跳过比较。
|
|
331
|
+
if (!Object.is(merged[key], expected)) continue
|
|
332
|
+
if (result === undefined) result = { ...merged }
|
|
333
|
+
delete result[key]
|
|
334
|
+
}
|
|
335
|
+
return result ?? merged
|
|
155
336
|
}
|
|
156
337
|
|
|
157
|
-
/** agent-request:对冻结的 LlmCallConfig 做浅合并 /
|
|
338
|
+
/** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
|
|
158
339
|
function wireAgentRequests(ctx, configs, warnOnce) {
|
|
340
|
+
const disposers = []
|
|
159
341
|
for (const config of configs) {
|
|
160
|
-
ctx.on('agent/request', async (payload, next) => {
|
|
342
|
+
disposers.push(ctx.on('agent/request', async (payload, next) => {
|
|
161
343
|
const base = await next()
|
|
162
344
|
try {
|
|
163
345
|
if (!matchesAgentScope(config, payload?.agent)) return base
|
|
164
|
-
|
|
165
|
-
? config.params.patch
|
|
166
|
-
: {}
|
|
167
|
-
if (config.params?.replace === true) return { ...patch }
|
|
168
|
-
return { ...base, ...patch }
|
|
346
|
+
return applyAgentRequestParams(config.params, base)
|
|
169
347
|
} catch (error) {
|
|
170
348
|
warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
171
349
|
return base
|
|
172
350
|
}
|
|
173
|
-
})
|
|
351
|
+
}))
|
|
174
352
|
}
|
|
353
|
+
return disposers
|
|
175
354
|
}
|
|
176
355
|
|
|
177
356
|
/** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
|
|
@@ -183,8 +362,9 @@ async function* replacedStream(text) {
|
|
|
183
362
|
|
|
184
363
|
/** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
|
|
185
364
|
function wireLlmStreams(ctx, configs, warnOnce) {
|
|
365
|
+
const disposers = []
|
|
186
366
|
for (const config of configs) {
|
|
187
|
-
ctx.on('llm/stream', (options, next) => {
|
|
367
|
+
disposers.push(ctx.on('llm/stream', (options, next) => {
|
|
188
368
|
try {
|
|
189
369
|
const mode = config.params?.mode ?? 'pass'
|
|
190
370
|
if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
|
|
@@ -195,18 +375,46 @@ function wireLlmStreams(ctx, configs, warnOnce) {
|
|
|
195
375
|
warnOnce(`${name}: llm-stream config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
196
376
|
return next()
|
|
197
377
|
}
|
|
198
|
-
})
|
|
378
|
+
}))
|
|
379
|
+
}
|
|
380
|
+
return disposers
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
|
|
384
|
+
|
|
385
|
+
/** 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。 */
|
|
386
|
+
export function pluginMessage(prefix, text, summary) {
|
|
387
|
+
return {
|
|
388
|
+
id: newMessageId(prefix),
|
|
389
|
+
role: 'user',
|
|
390
|
+
content: [{ type: 'text', text }],
|
|
391
|
+
source: { kind: 'plugin', plugin: name, form: 'notice', summary },
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 层级文本:按声明变量插值后按空行拼接;不走官方插值通道(非 system-section)。 */
|
|
396
|
+
function layerText(config, agent, warnOnce) {
|
|
397
|
+
try {
|
|
398
|
+
return config.texts
|
|
399
|
+
.map((item) => interpolateVariables(item, config.variables, agent?.session))
|
|
400
|
+
.filter((item) => item.length > 0)
|
|
401
|
+
.join('\n\n')
|
|
402
|
+
} catch (error) {
|
|
403
|
+
warnOnce(`${name}: ${config.layer} ${config.id} text failed: ${String(error?.message ?? error)}`)
|
|
404
|
+
return ''
|
|
199
405
|
}
|
|
200
406
|
}
|
|
201
407
|
|
|
202
408
|
/** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
|
|
203
409
|
function wireToolPipelines(ctx, configs, warnOnce) {
|
|
410
|
+
const disposers = []
|
|
204
411
|
for (const config of configs) {
|
|
205
412
|
const names = parseToolNames(config.params?.toolNames)
|
|
206
413
|
const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
|
|
207
|
-
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
414
|
+
disposers.push(ctx.on('tools/pre-execute', async (exec, next) => {
|
|
208
415
|
try {
|
|
209
416
|
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
417
|
+
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
|
|
210
418
|
const decision = config.params?.preDecision ?? 'allow'
|
|
211
419
|
if (decision === 'allow') return next()
|
|
212
420
|
if (decision === 'deny') {
|
|
@@ -218,10 +426,11 @@ function wireToolPipelines(ctx, configs, warnOnce) {
|
|
|
218
426
|
warnOnce(`${name}: tool-pipeline(pre) config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
219
427
|
return next()
|
|
220
428
|
}
|
|
221
|
-
})
|
|
222
|
-
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
429
|
+
}))
|
|
430
|
+
disposers.push(ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
223
431
|
try {
|
|
224
432
|
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
433
|
+
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
|
|
225
434
|
const action = config.params?.postAction ?? 'accept'
|
|
226
435
|
if (action === 'accept') return next()
|
|
227
436
|
if (action === 'replace' && config.texts.length > 0) {
|
|
@@ -235,15 +444,226 @@ function wireToolPipelines(ctx, configs, warnOnce) {
|
|
|
235
444
|
warnOnce(`${name}: tool-pipeline(post) config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
236
445
|
return next()
|
|
237
446
|
}
|
|
238
|
-
})
|
|
447
|
+
}))
|
|
448
|
+
}
|
|
449
|
+
return disposers
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 轮次停止层的续跑上限:每轮 1 次、每会话 3 次,均为引擎常量。
|
|
454
|
+
* 不暴露为配置——强制续跑失控会把会话卡在停不下来的循环里;官方桥在同一位置
|
|
455
|
+
* 也只留了 TODO(stop-loop-guard) 而未实现上限。
|
|
456
|
+
*/
|
|
457
|
+
export const TURN_STOP_MAX_PER_TURN = 1
|
|
458
|
+
export const TURN_STOP_MAX_PER_SESSION = 3
|
|
459
|
+
|
|
460
|
+
/** 会话内保留的轮次计数上限(与 deliberation-gate 同规模)。 */
|
|
461
|
+
export const TURN_STOP_MAX_TRACKED_TURNS = 8
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 续跑预算(唯一实现):B3 T2 第 (4) 类动作的「续跑」与既有 turn-stop 层共用它,
|
|
465
|
+
* 上限是引擎常量而不是配置——强制续跑失控会把会话卡在停不下来的循环里。
|
|
466
|
+
* 三步语义刻意与迁移前逐行一致:
|
|
467
|
+
* entry(...) 取条目(含创建与超限轮次淘汰的副作用),在条件判定**之前**调用;
|
|
468
|
+
* available(...) 只读判定,不落账;
|
|
469
|
+
* claim(...) 落账,必须在 steer **之前**调用(steer 抛错也不允许重试越过预算)。
|
|
470
|
+
*/
|
|
471
|
+
export function createTurnStopBudget() {
|
|
472
|
+
/**
|
|
473
|
+
* 会话预算条目(`sessionState`:统一访问接口,策略逐条声明)。
|
|
474
|
+
*
|
|
475
|
+
* 键类型 `session.id`;淘汰策略超限 `clear()` 全清(与迁移前的 `sessionMapGet` 同档:
|
|
476
|
+
* 第 `MAX_TRACKED_SESSIONS` 个会话仍会清空整个容器);**不声明复位**——预算是纯增量、
|
|
477
|
+
* 不可从事件流重建的计数(与 `progress-reminder` 同档),丢失时机必须与迁移前逐字一致,
|
|
478
|
+
* compaction 也不重置它。
|
|
479
|
+
*
|
|
480
|
+
* 本函数**没有 ctx**:`createTurnStopBudget()` 是 B3 交付物的一部分,对外签名与
|
|
481
|
+
* `entry/available/claim` 三个方法逐字不变(调用方见 actions.mjs 的 append-context 与
|
|
482
|
+
* 本文件的 turn-stop 层)。`sessionState` 只在声明了 `reset` 时才读 ctx,因此这里传
|
|
483
|
+
* `undefined` 合法且零开销(详见 shared.mjs 的 JSDoc 与 test/engine/session-state.test.mjs)。
|
|
484
|
+
*/
|
|
485
|
+
const state = sessionState(undefined, () => ({ turns: new Map(), total: 0 }))
|
|
486
|
+
|
|
487
|
+
const stateOf = (sessionId, turn) => {
|
|
488
|
+
// 对外仍是 `(sessionId, turn)`:按 id 造一次性键宿主,`keyOf` 取的就是 `.id`,与旧的
|
|
489
|
+
// `sessionMapGet(state, sessionId, …)` 写进同一个槽位、同一个 clear 时机。
|
|
490
|
+
// 唯一差异在**无 id 的会话**(`sessionId === undefined`):`sessionState` 刻意不记账
|
|
491
|
+
// (见 shared.mjs),旧写法把 `undefined` 当一个共享槽位。两个调用点(actions.mjs 的
|
|
492
|
+
// append-context 与下方 turn-stop 层)都在 `entry()` 之前守卫 `session?.id === undefined`,
|
|
493
|
+
// 故该路径不可达;这里兜底一个新条目,避免将来误用时抛 TypeError。
|
|
494
|
+
const entry = state.get({ id: sessionId }) ?? { turns: new Map(), total: 0 }
|
|
495
|
+
if (!Number.isFinite(turn)) return entry
|
|
496
|
+
if (!entry.turns.has(turn) && entry.turns.size >= TURN_STOP_MAX_TRACKED_TURNS) {
|
|
497
|
+
const oldest = [...entry.turns.keys()].sort((a, b) => a - b)
|
|
498
|
+
for (const key of oldest.slice(0, entry.turns.size - TURN_STOP_MAX_TRACKED_TURNS + 1)) entry.turns.delete(key)
|
|
499
|
+
}
|
|
500
|
+
if (!entry.turns.has(turn)) entry.turns.set(turn, 0)
|
|
501
|
+
return entry
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
entry: (sessionId, turn) => stateOf(sessionId, turn),
|
|
506
|
+
available: (entry, turn) => {
|
|
507
|
+
const turnCount = Number.isFinite(turn) ? (entry.turns.get(turn) ?? 0) : 0
|
|
508
|
+
return turnCount < TURN_STOP_MAX_PER_TURN && entry.total < TURN_STOP_MAX_PER_SESSION
|
|
509
|
+
},
|
|
510
|
+
claim: (entry, turn) => {
|
|
511
|
+
if (Number.isFinite(turn)) entry.turns.set(turn, (entry.turns.get(turn) ?? 0) + 1)
|
|
512
|
+
entry.total += 1
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。 */
|
|
518
|
+
function wireTurnStops(ctx, configs, warnOnce) {
|
|
519
|
+
const disposers = []
|
|
520
|
+
if (configs.length === 0) return disposers
|
|
521
|
+
const budget = createTurnStopBudget()
|
|
522
|
+
|
|
523
|
+
for (const config of configs) {
|
|
524
|
+
disposers.push(ctx.on('agent/turn-stopping', ({ agent, turn } = {}) => {
|
|
525
|
+
try {
|
|
526
|
+
const session = agent?.session
|
|
527
|
+
if (session?.id === undefined || typeof agent.steer !== 'function') return
|
|
528
|
+
if (!matchesAgentScope(config, agent)) return
|
|
529
|
+
const entry = budget.entry(session.id, turn)
|
|
530
|
+
if (!budget.available(entry, turn)) return
|
|
531
|
+
if (!conditionHit(config, { assistantText: lastAssistantText(session) })) return
|
|
532
|
+
const text = layerText(config, agent, warnOnce)
|
|
533
|
+
if (text.length === 0) return
|
|
534
|
+
// 计数在 steer 之前落账:steer 抛错也不允许下一步重试越过预算。
|
|
535
|
+
budget.claim(entry, turn)
|
|
536
|
+
agent.steer(pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`))
|
|
537
|
+
} catch (error) {
|
|
538
|
+
warnOnce(`${name}: turn-stop config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
539
|
+
}
|
|
540
|
+
}))
|
|
239
541
|
}
|
|
542
|
+
return disposers
|
|
240
543
|
}
|
|
241
544
|
|
|
242
|
-
/**
|
|
545
|
+
/** 沿官方持久 session 血缘找根会话,不根据当前 UI 会话或预设名称猜测。 */
|
|
546
|
+
function mainSessionForChild(ctx, id) {
|
|
547
|
+
const agents = getService(ctx, 'agents')
|
|
548
|
+
const sessions = getService(ctx, 'sessions')
|
|
549
|
+
const visited = new Set()
|
|
550
|
+
let session = agents?.get?.(id)?.session ?? sessions?.get?.(id)
|
|
551
|
+
if (session?.header?.parentSession === undefined) return undefined
|
|
552
|
+
while (session !== undefined) {
|
|
553
|
+
const sessionId = session.id ?? session.header?.id
|
|
554
|
+
if (typeof sessionId !== 'string' || visited.has(sessionId)) return undefined
|
|
555
|
+
visited.add(sessionId)
|
|
556
|
+
const parentId = session.header?.parentSession
|
|
557
|
+
if (parentId === undefined) return isDelegated(session) ? undefined : sessionId
|
|
558
|
+
session = agents?.get?.(parentId)?.session ?? sessions?.get?.(parentId)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** 子代理生命周期:启动注入子代理;结束默认观察,可显式给所属主会话投递上下文。 */
|
|
563
|
+
function wireSubagentEvents(ctx, configs, warnOnce) {
|
|
564
|
+
const disposers = []
|
|
565
|
+
const startConfigs = configs.filter((config) => config.layer === 'subagent-start')
|
|
566
|
+
const endConfigs = configs.filter((config) => config.layer === 'subagent-end')
|
|
567
|
+
const injectMain = endConfigs.some((config) => config.params?.action === 'inject-main')
|
|
568
|
+
// B4 保留原状(不迁 `sessionState`):键是 `runId`(运行标识)而不是 session,不属于
|
|
569
|
+
// 「按会话索引」的会话态,套统一接口会把语义挪到错误的键上;它的 `delete(最旧)` 也是与
|
|
570
|
+
// `clear()` 不同的另一档淘汰策略,无等价证明前保留。
|
|
571
|
+
const runs = new Map()
|
|
572
|
+
if (injectMain) {
|
|
573
|
+
disposers.push(ctx.on('subagent/start', (info) => {
|
|
574
|
+
if (typeof info?.runId !== 'string' || typeof info?.id !== 'string') return
|
|
575
|
+
const mainId = mainSessionForChild(ctx, info.id)
|
|
576
|
+
if (mainId === undefined) return
|
|
577
|
+
// ponytail: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
|
|
578
|
+
if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
|
|
579
|
+
runs.set(info.runId, { id: info.id, mainId, model: getService(ctx, 'agents')?.get?.(info.id)?.options?.model })
|
|
580
|
+
}))
|
|
581
|
+
ctx.effect?.(() => () => runs.clear())
|
|
582
|
+
}
|
|
583
|
+
if (startConfigs.length > 0) {
|
|
584
|
+
disposers.push(ctx.on('subagent/start', (info) => {
|
|
585
|
+
try {
|
|
586
|
+
const child = getService(ctx, 'agents')?.get?.(info?.id)
|
|
587
|
+
if (child === undefined) return
|
|
588
|
+
const subagentText = subagentTextOf(info)
|
|
589
|
+
for (const config of startConfigs) {
|
|
590
|
+
if (!matchesAgentScope(config, child)) continue
|
|
591
|
+
if (!conditionHit(config, { subagentText })) continue
|
|
592
|
+
if (typeof child.inject !== 'function') continue
|
|
593
|
+
const text = layerText(config, child, warnOnce)
|
|
594
|
+
if (text.length === 0) continue
|
|
595
|
+
child.inject(pluginMessage(`subagent-start-${config.id}`, text, `subagent-start ${config.id}`))
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
warnOnce(`${name}: subagent-start failed: ${String(error?.message ?? error)}`)
|
|
599
|
+
}
|
|
600
|
+
}))
|
|
601
|
+
}
|
|
602
|
+
if (endConfigs.length > 0) {
|
|
603
|
+
disposers.push(ctx.on('subagent/end', (info) => {
|
|
604
|
+
try {
|
|
605
|
+
const child = getService(ctx, 'agents')?.get?.(info?.id)
|
|
606
|
+
const recorded = runs.get(info?.runId)
|
|
607
|
+
runs.delete(info?.runId)
|
|
608
|
+
if (recorded !== undefined && recorded.id !== info?.id) return
|
|
609
|
+
const subagentText = subagentTextOf(info)
|
|
610
|
+
for (const config of endConfigs) {
|
|
611
|
+
if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
|
|
612
|
+
if (!conditionHit(config, { subagentText })) continue
|
|
613
|
+
if (config.params?.action !== 'inject-main') {
|
|
614
|
+
warnOnce(`${name}: subagent-end ${config.id} matched (observe only)`)
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
if (typeof info?.runId !== 'string' || info.runId.length === 0) continue
|
|
618
|
+
const mainId = recorded?.mainId ?? mainSessionForChild(ctx, info?.id)
|
|
619
|
+
const main = mainId === undefined ? undefined : getService(ctx, 'agents')?.get?.(mainId)
|
|
620
|
+
if (main?.session === undefined || typeof main.inject !== 'function') {
|
|
621
|
+
warnOnce(`${name}: subagent-end ${config.id} has no live main session`)
|
|
622
|
+
continue
|
|
623
|
+
}
|
|
624
|
+
const text = layerText(config, main, warnOnce)
|
|
625
|
+
if (text.length === 0) continue
|
|
626
|
+
let delivered = subagentEndDeliveries.get(main.session)
|
|
627
|
+
if (delivered === undefined) { delivered = new Map(); subagentEndDeliveries.set(main.session, delivered) }
|
|
628
|
+
const key = `${info.runId}:${config.id}`
|
|
629
|
+
if (delivered.has(key)) continue
|
|
630
|
+
// ponytail: 会话内保留最近的投递记录;超大历史改用持久事件索引时可取消此上限。
|
|
631
|
+
if (delivered.size >= MAX_TRACKED_SESSIONS) delivered.delete(delivered.keys().next().value)
|
|
632
|
+
delivered.set(key, true)
|
|
633
|
+
main.inject(pluginMessage(`subagent-end-${config.id}`, text, `subagent-end ${config.id}`))
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
warnOnce(`${name}: subagent-end failed: ${String(error?.message ?? error)}`)
|
|
637
|
+
}
|
|
638
|
+
}))
|
|
639
|
+
}
|
|
640
|
+
return disposers
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 把非 pre-step 提示词配置接入其声明的官方层级通道。
|
|
645
|
+
* @returns 聚合 disposer:回收本次接线显式创建的 waterfall 监听器;段/上下文/
|
|
646
|
+
* 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
|
|
647
|
+
*/
|
|
243
648
|
export function wireLayers(ctx, configs, warnOnce) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
649
|
+
// 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
|
|
650
|
+
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce)
|
|
651
|
+
const registered = [
|
|
652
|
+
wireSystemSections(ctx, configs.filter((config) => config.layer === 'system-section'), registry, warnOnce),
|
|
653
|
+
wireRuntimeContexts(ctx, configs.filter((config) => config.layer === 'runtime-context'), registry, warnOnce),
|
|
654
|
+
wireAgentRequests(ctx, configs.filter((config) => config.layer === 'agent-request'), warnOnce),
|
|
655
|
+
wireLlmStreams(ctx, configs.filter((config) => config.layer === 'llm-stream'), warnOnce),
|
|
656
|
+
wireToolPipelines(ctx, configs.filter((config) => config.layer === 'tool-pipeline'), warnOnce),
|
|
657
|
+
wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce),
|
|
658
|
+
wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce),
|
|
659
|
+
].flat().filter((disposer) => typeof disposer === 'function')
|
|
660
|
+
return () => {
|
|
661
|
+
for (const dispose of registered.splice(0)) {
|
|
662
|
+
try {
|
|
663
|
+
dispose()
|
|
664
|
+
} catch {
|
|
665
|
+
// 释放失败不得反过来打断卸载流程。
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
249
669
|
}
|