dsh-plugin-prompt-tool 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -29
- package/engine/actions/append-context.mjs +49 -0
- package/engine/actions/assembly.mjs +150 -0
- package/engine/actions/catalog.mjs +92 -0
- package/engine/actions/content.mjs +205 -0
- package/engine/actions/decision.mjs +54 -0
- package/engine/actions/examples.mjs +11 -0
- package/engine/actions/guard.mjs +80 -0
- package/engine/actions/inbox-prepend.mjs +37 -0
- package/engine/actions/index.mjs +100 -0
- package/engine/actions/inject-text.mjs +22 -0
- package/engine/actions/pre-step-filter.mjs +55 -0
- package/engine/actions/registration.mjs +168 -0
- package/engine/actions/request-params.mjs +32 -0
- package/engine/actions/sdk-strip.mjs +30 -0
- package/engine/actions/shared.mjs +71 -0
- package/engine/actions.mjs +2 -931
- package/engine/branch.mjs +44 -0
- package/engine/compaction-epoch.mjs +31 -13
- package/engine/compositions/source/local/dev-tool-search.yml +11 -0
- package/engine/compositions/source/local/instruction-hint.yml +22 -1
- package/engine/compositions/source/local/rule-engine.yml +6 -0
- package/engine/condition.mjs +16 -3
- package/engine/conditions/anchor.mjs +32 -0
- package/engine/conditions/availability.mjs +47 -0
- package/engine/conditions/composite.mjs +83 -0
- package/engine/conditions/count.mjs +140 -0
- package/engine/conditions/evaluation.mjs +41 -0
- package/engine/conditions/examples.mjs +11 -0
- package/engine/conditions/index.mjs +85 -0
- package/engine/conditions/names.mjs +21 -0
- package/engine/conditions/phase.mjs +54 -0
- package/engine/conditions/preset.mjs +94 -0
- package/engine/conditions/scope.mjs +18 -0
- package/engine/conditions/session.mjs +22 -0
- package/engine/conditions/source.mjs +63 -0
- package/engine/conditions/subject.mjs +121 -0
- package/engine/conditions/text.mjs +44 -0
- package/engine/conditions/values.mjs +32 -0
- package/engine/dev-tool-search.mjs +213 -0
- package/engine/executor.mjs +55 -6
- package/engine/fillers.mjs +23 -16
- package/engine/instruction-hint.mjs +34 -22
- package/engine/layers.mjs +135 -49
- package/engine/order.mjs +4 -1
- package/engine/predicates.mjs +2 -753
- package/engine/rule-engine.mjs +40 -0
- package/engine/rule-runtime.mjs +110 -0
- package/engine/rule-spec.mjs +187 -0
- package/engine/schema.mjs +16 -5
- package/engine/shared.mjs +1 -1
- package/engine/strategies.mjs +2 -235
- package/engine/subagent-tool-policy-core.mjs +7 -7
- package/engine/subagent-tool-policy.mjs +20 -10
- package/engine/tool-config-engine.mjs +53 -32
- package/engine/trigger-spec.mjs +49 -133
- package/lib/client.js +11117 -10860
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +82 -139
- package/lib/index.mjs +826 -1087
- package/lib/migrate-rules.d.mts +1 -0
- package/lib/migrate-rules.mjs +39 -0
- package/lib/module-package-CHPFweqK.mjs +2 -0
- package/lib/module-package-Cnd1_mwx.mjs +2872 -0
- package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
- package/lib/preset-transfer.d.mts +2 -2
- package/lib/preset-transfer.mjs +3 -2
- package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
- package/modules/ponytail/module.yml +381 -147
- package/modules/skill-surface/module.yml +74 -0
- package/modules/tool-surface/module.yml +67 -0
- package/package.json +5 -1
- package/templates/10-pre-step.yml +26 -22
- package/templates/14-first-turn-anchor.yml +25 -21
- package/templates/15-guide-auto.yml +34 -19
- package/templates/16-custom-fallback.yml +27 -15
- package/templates/18-placeholder-env-facts.yml +23 -14
- package/templates/20-system-section.yml +16 -10
- package/templates/30-runtime-context.yml +13 -7
- package/templates/40-agent-request.yml +11 -8
- package/templates/50-llm-stream.yml +14 -6
- package/templates/60-tool-pipeline.yml +15 -9
- package/templates/65-turn-stop.yml +19 -12
- package/templates/66-subagent-start.yml +12 -7
- package/templates/70-subagent-maintenance.yml +35 -29
- package/templates/75-subagent-end.yml +10 -5
- package/templates/80-tool-surface.yml +66 -0
- package/templates/policies/legacy-defaults.yml +34 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
- package/engine/prompt-config-engine.mjs +0 -104
- package/engine/trigger-editor-meta.mjs +0 -48
- package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
- package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
- package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
- package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
package/engine/layers.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactVa
|
|
|
19
19
|
import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
|
|
20
20
|
import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
|
|
21
21
|
import { compareConfigSequence, compareTextPlacement } from './order.mjs'
|
|
22
|
+
import { ruleFrame, ruleMatches } from './conditions/evaluation.mjs'
|
|
22
23
|
|
|
23
24
|
const name = 'prompt-config-engine'
|
|
24
25
|
// 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
|
|
@@ -67,7 +68,7 @@ function officialAliasOf(name) {
|
|
|
67
68
|
* 非法官方名(中文/大写/连字符)改写成确定性别名并改写文本引用;注册失败的名字退回静态解析。
|
|
68
69
|
* @returns 各配置独立的引用白名单与别名;同一作用域内等价绑定只注册一次。
|
|
69
70
|
*/
|
|
70
|
-
function registerOfficialVariables(ctx, configs, warnOnce) {
|
|
71
|
+
function registerOfficialVariables(ctx, configs, warnOnce, keep) {
|
|
71
72
|
const registries = new Map(configs.map((config) => [config, { protect: new Set(), alias: new Map(), registered: new Set() }]))
|
|
72
73
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
73
74
|
if (systemPrompt === undefined || typeof systemPrompt.variable !== 'function') return registries
|
|
@@ -110,7 +111,7 @@ function registerOfficialVariables(ctx, configs, warnOnce) {
|
|
|
110
111
|
usedNames.add(official)
|
|
111
112
|
const source = isFact ? name.toLowerCase() : name
|
|
112
113
|
const declaredValue = declared ? String(config.variables[name] ?? '') : undefined
|
|
113
|
-
|
|
114
|
+
keep(systemPrompt.variable(official, (context) => {
|
|
114
115
|
const session = context?.agent?.session
|
|
115
116
|
const override = getSessionVar(session, source)
|
|
116
117
|
const value = override !== undefined ? String(override) : declaredValue ?? runtimeFactValue(source, session) ?? ''
|
|
@@ -187,7 +188,7 @@ function textRegistrationName(config, field) {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
190
|
-
function wireSystemSections(ctx, configs, registry, warnOnce) {
|
|
191
|
+
function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
|
|
191
192
|
const disposers = []
|
|
192
193
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
193
194
|
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
@@ -210,7 +211,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
|
|
|
210
211
|
.filter((item) => item.length > 0)
|
|
211
212
|
.join('\n\n')
|
|
212
213
|
: groupText
|
|
213
|
-
|
|
214
|
+
keep(systemPrompt.section({
|
|
214
215
|
name: textRegistrationName(base, 'sectionName'),
|
|
215
216
|
order: base.order,
|
|
216
217
|
text,
|
|
@@ -218,7 +219,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
|
|
|
218
219
|
}), `${name}: section ${base.id}`)
|
|
219
220
|
// suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
|
|
220
221
|
if (base.params?.suppressRuntimeContext === true) {
|
|
221
|
-
|
|
222
|
+
keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
|
|
222
223
|
}
|
|
223
224
|
} catch (error) {
|
|
224
225
|
warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
@@ -227,8 +228,60 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
|
|
|
227
228
|
return disposers
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
async function resolvedContextText(ctx, config, context, registry, warnOnce) {
|
|
232
|
+
const agent = context.agent
|
|
233
|
+
const session = agent?.session
|
|
234
|
+
const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
|
|
235
|
+
if (context.signal?.aborted || resolved == null) return ''
|
|
236
|
+
const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
237
|
+
const rendered = config.texts.length > 0 ? interpolateVariables(config.texts.join('\n\n'), variables, session)
|
|
238
|
+
: typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
|
|
239
|
+
return officialChannelText(rendered, `runtime-context ${config.id}`, registry, warnOnce)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 条件化文本保留官方排序占位;仅在本次真实 assembly 填充,不缓存 context 身份。 */
|
|
243
|
+
function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
|
|
244
|
+
if (!configs.length) return []
|
|
245
|
+
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
246
|
+
if (systemPrompt === undefined) { warnOnce(`${name}: systemPrompt unavailable`); return [] }
|
|
247
|
+
const variable = `pt_rule_${newMessageId('text').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
|
|
248
|
+
const marker = `{{${variable}}}`
|
|
249
|
+
keep(systemPrompt.variable(variable, () => ''), `${name}: conditional text marker`)
|
|
250
|
+
const bindings = []
|
|
251
|
+
for (const layer of ['system-section', 'runtime-context']) for (const group of textLayerGroups(configs.filter(config => config.layer === layer))) {
|
|
252
|
+
const section = layer === 'system-section'
|
|
253
|
+
const base = group[0]
|
|
254
|
+
const entryName = textRegistrationName(base, section ? 'sectionName' : 'contextName')
|
|
255
|
+
keep(systemPrompt[section ? 'section' : 'context']({ name: entryName, order: base.order, text: marker }), `${name}: conditional ${entryName}`)
|
|
256
|
+
bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts' })
|
|
257
|
+
}
|
|
258
|
+
let active = true
|
|
259
|
+
keep(() => { active = false }, `${name}: conditional text lifecycle`)
|
|
260
|
+
return [on('system-prompt/assemble', async (assembly, context, next, invocation) => {
|
|
261
|
+
const frame = invocation ?? ruleFrame('system-prompt/assemble', [assembly, context], warnOnce)
|
|
262
|
+
for (const binding of bindings) {
|
|
263
|
+
const entry = assembly[binding.collection].find(item => item.name === binding.name && item.text === marker)
|
|
264
|
+
if (entry === undefined) continue
|
|
265
|
+
entry.text = ''
|
|
266
|
+
const blocks = []
|
|
267
|
+
for (const config of binding.group) {
|
|
268
|
+
if (!active || context.signal?.aborted || !ruleMatches(config.rule, frame) || !matchesAgentScope(config, context.agent)) continue
|
|
269
|
+
try {
|
|
270
|
+
const text = config.layer === 'runtime-context' && (config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
|
|
271
|
+
? await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
|
|
272
|
+
: configText(config, registry, warnOnce, context)
|
|
273
|
+
if (text.length) blocks.push(text)
|
|
274
|
+
} catch (error) { warnOnce(`${name}: rule text ${config.id} failed: ${String(error?.message ?? error)}`) }
|
|
275
|
+
}
|
|
276
|
+
if (active && !context.signal?.aborted) entry.text = blocks.join('\n\n')
|
|
277
|
+
}
|
|
278
|
+
return next()
|
|
279
|
+
})]
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
230
283
|
/** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
|
|
231
|
-
function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
|
|
284
|
+
function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
|
|
232
285
|
const disposers = []
|
|
233
286
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
234
287
|
if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
|
|
@@ -247,7 +300,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
|
|
|
247
300
|
const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
|
|
248
301
|
const text = dynamic ? render : render()
|
|
249
302
|
if (!dynamic && text.length === 0) continue
|
|
250
|
-
|
|
303
|
+
keep(systemPrompt.context({
|
|
251
304
|
name: textRegistrationName(base, 'contextName'),
|
|
252
305
|
order: base.order,
|
|
253
306
|
text,
|
|
@@ -265,12 +318,12 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
|
|
|
265
318
|
const slotText = `{{${slotVariable}}}`
|
|
266
319
|
const registered = new Map()
|
|
267
320
|
let active = true
|
|
268
|
-
|
|
269
|
-
|
|
321
|
+
keep(() => { active = false }, `${name}: runtime-context lifecycle`)
|
|
322
|
+
keep(systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
|
|
270
323
|
for (const config of placeholders) {
|
|
271
324
|
try {
|
|
272
325
|
const contextName = textRegistrationName(config, 'contextName')
|
|
273
|
-
|
|
326
|
+
keep(systemPrompt.context({
|
|
274
327
|
name: contextName,
|
|
275
328
|
order: config.order,
|
|
276
329
|
text: slotText,
|
|
@@ -280,22 +333,15 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
|
|
|
280
333
|
warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
281
334
|
}
|
|
282
335
|
}
|
|
283
|
-
disposers.push(
|
|
336
|
+
disposers.push(on('system-prompt/assemble', async (assembly, context, next) => {
|
|
284
337
|
const entries = assembly.contexts.filter(entry => registered.has(entry.name) && entry.text === slotText)
|
|
285
338
|
for (const entry of entries) {
|
|
286
339
|
const config = registered.get(entry.name)
|
|
287
340
|
entry.text = ''
|
|
288
341
|
if (!active || context.signal?.aborted) continue
|
|
289
342
|
try {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
|
|
293
|
-
if (!active || context.signal?.aborted || resolved === null || resolved === undefined) continue
|
|
294
|
-
const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
|
|
295
|
-
const rendered = config.texts.length > 0
|
|
296
|
-
? interpolateVariables(config.texts.join('\n\n'), variables, session)
|
|
297
|
-
: typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
|
|
298
|
-
entry.text = officialChannelText(rendered, `runtime-context ${config.id}`, registry.get(config), warnOnce)
|
|
343
|
+
const text = await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
|
|
344
|
+
if (active && !context.signal?.aborted) entry.text = text
|
|
299
345
|
} catch (error) {
|
|
300
346
|
warnOnce(`${name}: runtime-context ${config.id} resolve failed: ${String(error?.message ?? error)}`)
|
|
301
347
|
}
|
|
@@ -340,15 +386,16 @@ export function applyAgentRequestParams(params, base) {
|
|
|
340
386
|
}
|
|
341
387
|
|
|
342
388
|
/** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
|
|
343
|
-
function wireAgentRequests(ctx, configs, warnOnce) {
|
|
389
|
+
function wireAgentRequests(ctx, configs, warnOnce, on) {
|
|
344
390
|
if (configs.length === 0) return []
|
|
345
391
|
const ordered = configs.some(config => config.sequence !== undefined) ? configs : [...configs].reverse()
|
|
346
|
-
return [
|
|
392
|
+
return [on('agent/request', async (payload, next, invocation) => {
|
|
347
393
|
let result = await next()
|
|
394
|
+
const frame = invocation ?? ruleFrame('agent/request', [payload], warnOnce)
|
|
348
395
|
// 模块请求在 next 返回后按序合并;无文件来源的独立调用保持原回栈顺序。
|
|
349
396
|
for (const config of ordered) {
|
|
350
397
|
try {
|
|
351
|
-
if (matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
|
|
398
|
+
if (ruleMatches(config.rule, frame) && matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
|
|
352
399
|
} catch (error) {
|
|
353
400
|
warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
354
401
|
}
|
|
@@ -365,11 +412,15 @@ async function* replacedStream(text) {
|
|
|
365
412
|
}
|
|
366
413
|
|
|
367
414
|
/** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
|
|
368
|
-
function wireLlmStreams(ctx, configs, warnOnce) {
|
|
415
|
+
function wireLlmStreams(ctx, configs, warnOnce, on) {
|
|
369
416
|
const disposers = []
|
|
370
417
|
for (const config of configs) {
|
|
371
|
-
disposers.push(
|
|
418
|
+
disposers.push(on('llm/stream', (options, next, invocation) => {
|
|
372
419
|
try {
|
|
420
|
+
const frame = invocation ?? ruleFrame('llm/stream', [options], warnOnce)
|
|
421
|
+
const agent = options?.sessionId === undefined ? undefined : getService(ctx, 'agents')?.get?.(options.sessionId)
|
|
422
|
+
frame.subject.agent = agent
|
|
423
|
+
if (!ruleMatches(config.rule, frame)) return next()
|
|
373
424
|
const mode = config.params?.mode ?? 'pass'
|
|
374
425
|
if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
|
|
375
426
|
return replacedStream(config.texts.join('\n\n'))
|
|
@@ -386,13 +437,21 @@ function wireLlmStreams(ctx, configs, warnOnce) {
|
|
|
386
437
|
|
|
387
438
|
/** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
|
|
388
439
|
|
|
389
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。
|
|
442
|
+
*
|
|
443
|
+
* `kind` 必须是**生产者名**而不是裸 `plugin`:会话格式 v4 把 `{ kind: 'plugin', plugin }`
|
|
444
|
+
* 列为退役包装并直接拒绝(`dsh-session-format-v3-to-v4` 的 `assertV4MessageSources` /
|
|
445
|
+
* `assertV4SourceRowAdmission` 一律抛 `format v4 message requires a producer-owned source kind`),
|
|
446
|
+
* 迁移只在读旧日志时把 `plugin` 折进 kind。写 `kind: 'plugin'` 会让本次 splice 被 codec
|
|
447
|
+
* 拒绝、连 `agent/inbox/spliced` 都落不了盘——子代理启动因此整段失败。
|
|
448
|
+
*/
|
|
390
449
|
export function pluginMessage(prefix, text, summary) {
|
|
391
450
|
return {
|
|
392
451
|
id: newMessageId(prefix),
|
|
393
452
|
role: 'user',
|
|
394
453
|
content: [{ type: 'text', text }],
|
|
395
|
-
source: { kind:
|
|
454
|
+
source: { kind: `plugin:${name}`, plugin: name, form: 'notice', summary },
|
|
396
455
|
}
|
|
397
456
|
}
|
|
398
457
|
|
|
@@ -410,13 +469,14 @@ function layerText(config, agent, warnOnce) {
|
|
|
410
469
|
}
|
|
411
470
|
|
|
412
471
|
/** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
|
|
413
|
-
function wireToolPipelines(ctx, configs, warnOnce) {
|
|
472
|
+
function wireToolPipelines(ctx, configs, warnOnce, on) {
|
|
414
473
|
const disposers = []
|
|
415
474
|
for (const config of configs) {
|
|
416
475
|
const names = parseToolNames(config.params?.toolNames)
|
|
417
476
|
const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
|
|
418
|
-
disposers.push(
|
|
477
|
+
disposers.push(on('tools/pre-execute', async (exec, next, invocation) => {
|
|
419
478
|
try {
|
|
479
|
+
if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/pre-execute', [exec], warnOnce))) return next()
|
|
420
480
|
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
421
481
|
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
|
|
422
482
|
const decision = config.params?.preDecision ?? 'allow'
|
|
@@ -431,8 +491,9 @@ function wireToolPipelines(ctx, configs, warnOnce) {
|
|
|
431
491
|
return next()
|
|
432
492
|
}
|
|
433
493
|
}))
|
|
434
|
-
disposers.push(
|
|
494
|
+
disposers.push(on('tools/post-execute', async (exec, result, next, invocation) => {
|
|
435
495
|
try {
|
|
496
|
+
if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/post-execute', [exec, result], warnOnce))) return next()
|
|
436
497
|
if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
|
|
437
498
|
if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
|
|
438
499
|
const action = config.params?.postAction ?? 'accept'
|
|
@@ -519,7 +580,7 @@ export function createTurnStopBudget() {
|
|
|
519
580
|
}
|
|
520
581
|
|
|
521
582
|
/** turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。 */
|
|
522
|
-
function wireTurnStops(ctx, configs, warnOnce) {
|
|
583
|
+
function wireTurnStops(ctx, configs, warnOnce, on) {
|
|
523
584
|
const disposers = []
|
|
524
585
|
if (configs.length === 0) return disposers
|
|
525
586
|
const budgets = new Map()
|
|
@@ -528,8 +589,10 @@ function wireTurnStops(ctx, configs, warnOnce) {
|
|
|
528
589
|
const source = config.sourceModuleId ?? ''
|
|
529
590
|
if (!budgets.has(source)) budgets.set(source, createTurnStopBudget())
|
|
530
591
|
const budget = budgets.get(source)
|
|
531
|
-
disposers.push(
|
|
592
|
+
disposers.push(on('agent/turn-stopping', (payload = {}, _next, invocation) => {
|
|
593
|
+
const { agent, turn } = payload
|
|
532
594
|
try {
|
|
595
|
+
if (!ruleMatches(config.rule, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
|
|
533
596
|
const session = agent?.session
|
|
534
597
|
if (session?.id === undefined || typeof agent.steer !== 'function') return
|
|
535
598
|
if (!matchesAgentScope(config, agent)) return
|
|
@@ -567,33 +630,39 @@ function mainSessionForChild(ctx, id) {
|
|
|
567
630
|
}
|
|
568
631
|
|
|
569
632
|
/** 子代理生命周期:启动注入子代理;结束默认观察,可显式给所属主会话投递上下文。 */
|
|
570
|
-
function wireSubagentEvents(ctx, configs, warnOnce) {
|
|
633
|
+
function wireSubagentEvents(ctx, configs, warnOnce, on) {
|
|
571
634
|
const disposers = []
|
|
572
635
|
const startConfigs = configs.filter((config) => config.layer === 'subagent-start')
|
|
573
636
|
const endConfigs = configs.filter((config) => config.layer === 'subagent-end')
|
|
574
|
-
const injectMain = endConfigs.some((config) => config.params?.action === 'inject-main')
|
|
575
637
|
// B4 保留原状(不迁 `sessionState`):键是 `runId`(运行标识)而不是 session,不属于
|
|
576
638
|
// 「按会话索引」的会话态,套统一接口会把语义挪到错误的键上;它的 `delete(最旧)` 也是与
|
|
577
639
|
// `clear()` 不同的另一档淘汰策略,无等价证明前保留。
|
|
578
640
|
const runs = new Map()
|
|
579
|
-
if (
|
|
641
|
+
if (endConfigs.length > 0) {
|
|
580
642
|
disposers.push(ctx.on('subagent/start', (info) => {
|
|
581
643
|
if (typeof info?.runId !== 'string' || typeof info?.id !== 'string') return
|
|
644
|
+
const child = getService(ctx, 'agents')?.get?.(info.id)
|
|
645
|
+
const session = child?.session ?? getService(ctx, 'sessions')?.get?.(info.id)
|
|
646
|
+
if (session === undefined) return
|
|
582
647
|
const mainId = mainSessionForChild(ctx, info.id)
|
|
583
|
-
if (mainId === undefined) return
|
|
584
648
|
// ponytail: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
|
|
585
649
|
if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
|
|
586
|
-
runs.set(info.runId, { id: info.id, mainId,
|
|
650
|
+
runs.set(info.runId, { id: info.id, mainId, agent: child, session, model: child?.options?.model })
|
|
587
651
|
}))
|
|
588
|
-
|
|
652
|
+
const clearRuns = () => runs.clear()
|
|
653
|
+
disposers.push(clearRuns)
|
|
654
|
+
ctx.effect?.(() => clearRuns)
|
|
589
655
|
}
|
|
590
656
|
if (startConfigs.length > 0) {
|
|
591
|
-
disposers.push(
|
|
657
|
+
disposers.push(on('subagent/start', (info, _next, invocation) => {
|
|
592
658
|
try {
|
|
593
659
|
const child = getService(ctx, 'agents')?.get?.(info?.id)
|
|
594
660
|
if (child === undefined) return
|
|
661
|
+
const frame = invocation ?? ruleFrame('subagent/start', [info], warnOnce)
|
|
662
|
+
frame.subject.agent = child
|
|
595
663
|
const subagentText = subagentTextOf(info)
|
|
596
664
|
for (const config of startConfigs) {
|
|
665
|
+
if (!ruleMatches(config.rule, frame)) continue
|
|
597
666
|
if (!matchesAgentScope(config, child)) continue
|
|
598
667
|
if (!conditionHit(config, { subagentText })) continue
|
|
599
668
|
if (typeof child.inject !== 'function') continue
|
|
@@ -607,14 +676,19 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
|
|
|
607
676
|
}))
|
|
608
677
|
}
|
|
609
678
|
if (endConfigs.length > 0) {
|
|
610
|
-
disposers.push(
|
|
679
|
+
disposers.push(on('subagent/end', (info, _next, invocation) => {
|
|
611
680
|
try {
|
|
612
681
|
const child = getService(ctx, 'agents')?.get?.(info?.id)
|
|
613
682
|
const recorded = runs.get(info?.runId)
|
|
614
683
|
runs.delete(info?.runId)
|
|
615
684
|
if (recorded !== undefined && recorded.id !== info?.id) return
|
|
616
685
|
const subagentText = subagentTextOf(info)
|
|
686
|
+
const frame = invocation ?? ruleFrame('subagent/end', [info], warnOnce)
|
|
687
|
+
frame.subject.agent = child ?? recorded?.agent
|
|
688
|
+
frame.subject.session = child?.session ?? recorded?.session ?? getService(ctx, 'sessions')?.get?.(info?.id)
|
|
689
|
+
frame.subject.model = child?.options?.model ?? recorded?.model
|
|
617
690
|
for (const config of endConfigs) {
|
|
691
|
+
if (!ruleMatches(config.rule, frame)) continue
|
|
618
692
|
if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
|
|
619
693
|
if (!conditionHit(config, { subagentText })) continue
|
|
620
694
|
if (config.params?.action !== 'inject-main') {
|
|
@@ -652,18 +726,30 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
|
|
|
652
726
|
* @returns 聚合 disposer:回收本次接线显式创建的 waterfall 监听器;段/上下文/
|
|
653
727
|
* 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
|
|
654
728
|
*/
|
|
655
|
-
export function wireLayers(ctx, configs, warnOnce) {
|
|
729
|
+
export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
656
730
|
configs = [...configs].sort(compareConfigSequence)
|
|
731
|
+
const owned = []
|
|
732
|
+
const keep = (dispose, label) => {
|
|
733
|
+
if (typeof dispose !== 'function') return
|
|
734
|
+
let active = true
|
|
735
|
+
const release = () => { if (active) { active = false; dispose() } }
|
|
736
|
+
owned.push(release); keepDisposer(ctx, release, label)
|
|
737
|
+
}
|
|
738
|
+
const on = options.on ?? ((...args) => ctx.on(...args))
|
|
657
739
|
// 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
|
|
658
|
-
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce)
|
|
740
|
+
const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep)
|
|
741
|
+
const conditional = configs.filter(config => typeof config.rule?.when === 'function' && ['system-section', 'runtime-context'].includes(config.layer))
|
|
742
|
+
const regular = configs.filter(config => !conditional.includes(config))
|
|
659
743
|
const registered = [
|
|
660
|
-
wireSystemSections(ctx,
|
|
661
|
-
wireRuntimeContexts(ctx,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
744
|
+
wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep),
|
|
745
|
+
wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on),
|
|
746
|
+
wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on),
|
|
747
|
+
wireAgentRequests(ctx, configs.filter((config) => config.layer === 'agent-request'), warnOnce, on),
|
|
748
|
+
wireLlmStreams(ctx, configs.filter((config) => config.layer === 'llm-stream'), warnOnce, on),
|
|
749
|
+
wireToolPipelines(ctx, configs.filter((config) => config.layer === 'tool-pipeline'), warnOnce, on),
|
|
750
|
+
wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on),
|
|
751
|
+
wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on),
|
|
752
|
+
owned,
|
|
667
753
|
].flat().filter((disposer) => typeof disposer === 'function')
|
|
668
754
|
return () => {
|
|
669
755
|
for (const dispose of registered.splice(0)) {
|
package/engine/order.mjs
CHANGED
|
@@ -8,7 +8,10 @@ export function compareConfigSequence(a, b) {
|
|
|
8
8
|
const difference = (a.sequence ?? a.order ?? 0) - (b.sequence ?? b.order ?? 0)
|
|
9
9
|
if (difference !== 0) return difference
|
|
10
10
|
if (a.sourceModuleId === undefined && b.sourceModuleId === undefined) return 0
|
|
11
|
-
return compareText(a.sourceModuleId ?? '', b.sourceModuleId ?? '')
|
|
11
|
+
return compareText(a.sourceModuleId ?? '', b.sourceModuleId ?? '')
|
|
12
|
+
|| compareText(a.ruleId ?? a.id, b.ruleId ?? b.id)
|
|
13
|
+
|| (a.ruleActionIndex ?? 0) - (b.ruleActionIndex ?? 0)
|
|
14
|
+
|| compareText(a.id, b.id)
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/** 官方文本段的定位仍由 order 决定;序号只提供同位置的稳定顺序。 */
|