dsh-plugin-prompt-tool 2.1.0 → 3.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 +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 +161 -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 +109 -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/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -0
- package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -0
- package/modules/soul/assets/reference/README.md +12 -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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-visibility —— 工具可见面:一份声明拥有「模型看得见什么、调得动什么」。
|
|
3
|
+
*
|
|
4
|
+
* 三面投影都在 system-prompt/assemble 的 after-next 之后完成:
|
|
5
|
+
* 1. 装配目录(tools 数组)——本轮的呈现面;
|
|
6
|
+
* 2. tools:sdk 段正文——模型读到的工具声明;
|
|
7
|
+
* 3. 执行面:deny 用 restrict + guard;allow 只登记 guard(restrict 无法安全覆盖预设/祖先层)。
|
|
8
|
+
*
|
|
9
|
+
* 旧实现把同一个意图拆成三条声明(assembly / sdk-strip / guard),其中 guard 属固定
|
|
10
|
+
* 注册效果、不能带规则级 if,于是「有条件下架」被迫再复制一份名单。这里把名单收在
|
|
11
|
+
* 一个动作里:条件由规则引擎在装配通道逐轮求值,执行闸按当前条件**重挂或撤销**,
|
|
12
|
+
* 因此不再有跨轮粘性。
|
|
13
|
+
*
|
|
14
|
+
* 名单语义对齐官方 ToolRestriction(deepseek-harness packages/core/tools/src/index.ts:701-706):
|
|
15
|
+
* allow 与 deny 可以同时给,判据是「先按 allow 保留,再按 deny 剔除」。requireMatch 只在
|
|
16
|
+
* allow 模式有意义:名单里任一工具在目录与 SDK 正文的并集里都找不到,就放弃裁剪、暴露完整目录(fail-open)并告警——
|
|
17
|
+
* 收窄错了会让当前工作流调不到工具,比多花上下文严重。
|
|
18
|
+
*
|
|
19
|
+
* 执行边界:deny 同时用 restrict(继承面)与 guard(本层/晚到);allow 不用 restrict——
|
|
20
|
+
* 它的 allow 只能收继承面名字,而 `schemas()` 全局视图会漏掉预设/祖先层,局部 allow 会
|
|
21
|
+
* 把要保留的工具裁掉。allow 统一由 guard 裁决;requireMatch fail-open 时 guard 同步放行。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { RUN_CODE, createMask, labelOf, toolSurfacePlan } from './shared.mjs'
|
|
25
|
+
import { createAgentCapability } from '../agent-capability.mjs'
|
|
26
|
+
import { SDK_SECTION_NAME, stripSdkDeclarations, sdkToolNames } from '../sdk-strip.mjs'
|
|
27
|
+
import { TOOL_INVOCATION_EVENT_TYPES, historyEvents } from '../history.mjs'
|
|
28
|
+
import { toolScopeSnapshot } from '../tool-scope.mjs'
|
|
29
|
+
import { DISCOVERY_SERVICE } from '../service-names.mjs'
|
|
30
|
+
import { UNLOCKABLE_DISCOVERY_KINDS } from '../discovery.mjs'
|
|
31
|
+
|
|
32
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
+
|
|
34
|
+
function parseDiscovery(action, label, plugin) {
|
|
35
|
+
const discovery = action.discovery
|
|
36
|
+
if (discovery === undefined) return undefined
|
|
37
|
+
if (!record(discovery)) throw new TypeError(`${plugin}: ${label}.discovery must be an object — { kind, tool?, key? }`)
|
|
38
|
+
const kind = discovery.kind
|
|
39
|
+
if (typeof kind !== 'string' || kind.length === 0) {
|
|
40
|
+
throw new TypeError(`${plugin}: ${label}.discovery.kind is required`)
|
|
41
|
+
}
|
|
42
|
+
// 只有支持「按名解锁」的数据源才能被可见面消费:技能面没有解锁名单语义。
|
|
43
|
+
if (!UNLOCKABLE_DISCOVERY_KINDS.has(kind)) {
|
|
44
|
+
throw new TypeError(`${plugin}: ${label}.discovery.kind ${JSON.stringify(kind)} has no unlock semantics — 可用:${[...UNLOCKABLE_DISCOVERY_KINDS].join(', ')}`)
|
|
45
|
+
}
|
|
46
|
+
const result = { kind }
|
|
47
|
+
for (const field of ['tool', 'key']) {
|
|
48
|
+
if (discovery[field] === undefined) continue
|
|
49
|
+
if (typeof discovery[field] !== 'string' || discovery[field].length === 0) {
|
|
50
|
+
throw new TypeError(`${plugin}: ${label}.discovery.${field} must be a non-empty string`)
|
|
51
|
+
}
|
|
52
|
+
// 工具身份归 adapter 元数据;声明里写这两个键只作断言(不匹配时告警,不改变实际行为)。
|
|
53
|
+
result[field] = discovery[field]
|
|
54
|
+
}
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/** 同一份 mask 的稳定身份:名单变化必须重挂执行闸,不能沿用旧名单。 */
|
|
60
|
+
function maskKey(mask) {
|
|
61
|
+
const list = (set) => set === undefined ? '' : [...set].sort().join(',')
|
|
62
|
+
return `allow:${list(mask.allow)}|deny:${list(mask.deny)}|require:${mask.requireMatch ? 1 : 0}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 从完整历史回收解锁名(判据与旧 assembly.allowFrom 逐条对齐):认顶层 `tool/call`
|
|
67
|
+
* 与 PTC 嵌套 `tool/ptc-dispatch` 里精确等于发现工具的调用,参数解析失败逐条跳过;
|
|
68
|
+
* 解锁只做加法,不能越过 deny。读完整历史而非可见面:压缩遮蔽那条调用后仍要保留。
|
|
69
|
+
*/
|
|
70
|
+
function fillUnlocked(into, session, tool, key, warnOnce, plugin, label) {
|
|
71
|
+
into.clear()
|
|
72
|
+
// 发现工具是解锁通道本身:它必须恒在放行集里,否则白名单会把它一起裁掉、从此无法解锁。
|
|
73
|
+
into.add(tool)
|
|
74
|
+
let events
|
|
75
|
+
try {
|
|
76
|
+
events = historyEvents(session)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: reading session events for discovery failed: ${String(error?.message ?? error)}`)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
// PTC 下 run_code 里的嵌套调用落为 tool/ptc-dispatch,不是顶层 tool/call。
|
|
83
|
+
if (!TOOL_INVOCATION_EVENT_TYPES.includes(event?.type) || event?.data?.name !== tool) continue
|
|
84
|
+
const raw = event.data.arguments
|
|
85
|
+
let parsed
|
|
86
|
+
try { parsed = typeof raw === 'string' ? JSON.parse(raw) : raw } catch { continue }
|
|
87
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue
|
|
88
|
+
const value = parsed[key]
|
|
89
|
+
if (!Array.isArray(value)) continue
|
|
90
|
+
for (const item of value) if (typeof item === 'string' && item.length > 0) into.add(item)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function prepareToolVisibility(action, plugin) {
|
|
95
|
+
const label = labelOf(action)
|
|
96
|
+
const discovery = parseDiscovery(action, label, plugin)
|
|
97
|
+
// 发现工具只解锁白名单里没点的名字:allowExtra 是「只做加法」的活集合,每轮从持久调用记录回收。
|
|
98
|
+
if (discovery !== undefined && action.allow === undefined) {
|
|
99
|
+
throw new TypeError(`${plugin}: ${label}.discovery requires allow — 解锁只对白名单有意义`)
|
|
100
|
+
}
|
|
101
|
+
const allowExtra = discovery === undefined ? undefined : new Set()
|
|
102
|
+
const mask = createMask(action, label, plugin, allowExtra)
|
|
103
|
+
// requireMatch 只对白名单有意义(与 assembly / sdk-strip 同判据):黑名单没有「必需名单」。
|
|
104
|
+
if (mask.requireMatch && mask.allow === undefined) {
|
|
105
|
+
throw new TypeError(`${plugin}: ${label}.requireMatch only applies to allow — deny lists have no required names`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (mountCtx, { warnOnce, on, collect, onMiss }) => {
|
|
109
|
+
/** 执行面:restrict 覆盖继承的全局工具,guard 兜住本层与晚到工具。 */
|
|
110
|
+
const capability = createAgentCapability((agent) => {
|
|
111
|
+
const tools = agent?.ctx?.tools
|
|
112
|
+
if (tools === undefined) return undefined
|
|
113
|
+
const state = { failOpen: false }
|
|
114
|
+
const disposers = []
|
|
115
|
+
try {
|
|
116
|
+
const known = toolScopeSnapshot(tools).globalNames
|
|
117
|
+
if (mask.deny !== undefined && typeof tools.restrict === 'function' && known !== undefined) {
|
|
118
|
+
// allow 模式不走 restrict:`schemas()` 是全局视图,过滤后的 allow 会漏掉预设/祖先层的
|
|
119
|
+
// 工具,restrict 反而把它们裁掉;restrict 只接收继承面名字,无法安全区分本层工具。
|
|
120
|
+
// allow 的执行面统一由下面的 guard 兜底。
|
|
121
|
+
const deny = [...mask.deny].filter((name) => known.has(name))
|
|
122
|
+
if (deny.length > 0) {
|
|
123
|
+
try {
|
|
124
|
+
const dispose = tools.restrict({ deny })
|
|
125
|
+
if (typeof dispose === 'function') disposers.push(dispose)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// restrict 失败不改变呈现面;执行面还有 guard 兜底。
|
|
128
|
+
warnOnce(`${plugin}: tool-visibility action ${label} restrict skipped: ${String(error?.message ?? error)}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ((mask.deny !== undefined || mask.allow !== undefined) && typeof tools.guard === 'function') {
|
|
133
|
+
const deny = mask.deny
|
|
134
|
+
disposers.push(tools.guard((exec) => {
|
|
135
|
+
const toolName = exec?.name
|
|
136
|
+
if (toolName === RUN_CODE || typeof toolName !== 'string' || toolName.length === 0) return undefined
|
|
137
|
+
if (deny?.has(toolName)) return `${label}: tool ${JSON.stringify(toolName)} is denied by tool-visibility`
|
|
138
|
+
if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName)) {
|
|
139
|
+
return `${label}: tool ${JSON.stringify(toolName)} is not allowed by tool-visibility`
|
|
140
|
+
}
|
|
141
|
+
return undefined
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// 声明失败不能丢已注册的 restrict:已收集的 disposer 仍交给声明表统一撤销。
|
|
146
|
+
warnOnce(`${plugin}: tool-visibility action ${label} failed to mount execution gate: ${String(error?.message ?? error)}`)
|
|
147
|
+
}
|
|
148
|
+
return { disposers, data: state }
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const syncGate = (agent) => {
|
|
152
|
+
const key = `${maskKey(mask)}|unlock:${allowExtra === undefined ? '' : [...allowExtra].sort().join(',')}`
|
|
153
|
+
capability.sync(agent, key)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
collect(capability.releaseAll)
|
|
157
|
+
// 条件为假时 rule-runtime 不进入 handler:未命中回调负责撤掉上一轮注册的执行闸。
|
|
158
|
+
// 动作级 when 由 withWhen 调用时没有帧,只能撤当前已登记的全部会话。
|
|
159
|
+
onMiss((frame) => {
|
|
160
|
+
const session = frame?.subject?.agent?.session ?? frame?.args?.[1]?.agent?.session
|
|
161
|
+
if (session === undefined) capability.releaseAll()
|
|
162
|
+
else capability.release(session)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
collect(on('system-prompt/assemble', async (assembly, context, next) => {
|
|
166
|
+
const assembled = await next()
|
|
167
|
+
// 解锁名必须先回收:执行闸的 restrict allow 要包含本轮解锁项,否则解锁的工具看得见、调不动。
|
|
168
|
+
// 工具名与参数键来自发现注册表的 adapter 元数据;声明里的同名字段只作断言。
|
|
169
|
+
if (allowExtra !== undefined) {
|
|
170
|
+
const registry = typeof mountCtx.get === 'function' ? mountCtx.get(DISCOVERY_SERVICE) : undefined
|
|
171
|
+
const adapter = typeof registry?.get === 'function' ? registry.get(context?.agent, discovery.kind) : undefined
|
|
172
|
+
const unlock = adapter?.unlock
|
|
173
|
+
if (unlock === undefined) {
|
|
174
|
+
allowExtra.clear()
|
|
175
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: discovery adapter "${discovery.kind}" is not registered for this agent — unlock recovery skipped`)
|
|
176
|
+
} else {
|
|
177
|
+
if (discovery.tool !== undefined && discovery.tool !== unlock.tool) {
|
|
178
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: discovery.tool ${JSON.stringify(discovery.tool)} does not match the "${discovery.kind}" adapter tool ${JSON.stringify(unlock.tool)}`)
|
|
179
|
+
}
|
|
180
|
+
if (discovery.key !== undefined && discovery.key !== unlock.key) {
|
|
181
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: discovery.key ${JSON.stringify(discovery.key)} does not match the "${discovery.kind}" adapter key ${JSON.stringify(unlock.key)}`)
|
|
182
|
+
}
|
|
183
|
+
fillUnlocked(allowExtra, context?.session ?? context?.agent?.session, unlock.tool, unlock.key, warnOnce, plugin, label)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
syncGate(context?.agent)
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// 闸注册失败必须留痕;呈现面照常结算,绝不把异常抛给装配。
|
|
190
|
+
warnOnce(`${plugin}: tool-visibility action ${label} failed to sync execution gate: ${String(error?.message ?? error)}`)
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
let result = assembled
|
|
194
|
+
// PTC 的 tools 数组只有 run_code 传输;实际可见面在 SDK 正文里,requireMatch 必须
|
|
195
|
+
// 以两者并集为准,否则 native 的收窄名单会被误判成缺失并 fail-open。
|
|
196
|
+
const sdkSection = Array.isArray(result.sections)
|
|
197
|
+
? result.sections.find((section) => section?.name === SDK_SECTION_NAME && typeof section.text === 'string')
|
|
198
|
+
: undefined
|
|
199
|
+
const sdkNames = sdkSection === undefined ? undefined : new Set(sdkToolNames(sdkSection.text))
|
|
200
|
+
const available = new Set([
|
|
201
|
+
...(Array.isArray(result.tools) ? result.tools.map((tool) => tool?.name).filter((name) => typeof name === 'string') : []),
|
|
202
|
+
...(sdkNames ?? []),
|
|
203
|
+
])
|
|
204
|
+
// 没有工具面可观(既无目录也无 SDK 正文)时不裁剪、不告警、不动执行闸:
|
|
205
|
+
// 空 surface 不等于「名单全缺」;空目录(tools=[])仍是真实观测,照旧 fail-open。
|
|
206
|
+
if (!Array.isArray(result.tools) && sdkSection === undefined) return result
|
|
207
|
+
// 三面(目录 / 正文 / 执行闸)共用这一份判断:缺项就整体放宽,呈现与执行不得两套口径。
|
|
208
|
+
const plan = toolSurfacePlan(mask, available)
|
|
209
|
+
const state = capability.get(context?.session ?? context?.agent?.session)
|
|
210
|
+
if (state !== undefined) state.data.failOpen = plan.failOpen
|
|
211
|
+
if (plan.failOpen) {
|
|
212
|
+
// 去重键固定到动作:缺项名随解锁历史变化,不能让每个新集合各占一条 warnOnce。
|
|
213
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: requireMatch tools absent (${plan.missing.join(', ')}) — exposing the full tool set (fail-open)`, `${plugin}: tool-visibility action ${label}: requireMatch fail-open`)
|
|
214
|
+
} else {
|
|
215
|
+
if (Array.isArray(result.tools)) {
|
|
216
|
+
const kept = result.tools.filter((tool) => !mask.blocks(tool?.name))
|
|
217
|
+
if (kept.length !== result.tools.length) result = { ...result, tools: kept }
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(result.sections)) {
|
|
220
|
+
let changed = false
|
|
221
|
+
let present = sdkNames
|
|
222
|
+
const sections = result.sections.map((section) => {
|
|
223
|
+
if (section?.name !== SDK_SECTION_NAME || typeof section.text !== 'string') return section
|
|
224
|
+
const presentNames = () => {
|
|
225
|
+
present ??= new Set(sdkToolNames(section.text))
|
|
226
|
+
return present
|
|
227
|
+
}
|
|
228
|
+
const names = mask.allow === undefined
|
|
229
|
+
? mask.deny
|
|
230
|
+
: new Set([...presentNames()].filter((name) => mask.blocks(name)))
|
|
231
|
+
if (names === undefined || names.size === 0) return section
|
|
232
|
+
const stripped = stripSdkDeclarations(section.text, names)
|
|
233
|
+
if (stripped === section.text) return section
|
|
234
|
+
changed = true
|
|
235
|
+
return { ...section, text: stripped }
|
|
236
|
+
})
|
|
237
|
+
if (changed) result = { ...result, sections }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// 呈现面失败只告警:执行闸已在上面同步过,deny 仍然拦得住。
|
|
243
|
+
warnOnce(`${plugin}: tool-visibility action ${label} failed, keeping the assembled surface: ${String(error?.message ?? error)}`)
|
|
244
|
+
return assembled
|
|
245
|
+
}
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 级能力声明原语:受众判定 + Agent scope 声明/撤销 + 条件翻转的幂等重挂。
|
|
3
|
+
*
|
|
4
|
+
* 只收这三件事。冲突检测与晚到服务等待留给调用方:presentAs 的同 scope 二次声明是
|
|
5
|
+
* 官方抛错语义,ptcRuntime 的晚到等待是一次性声明;把它们塞进同一接口会逼出模式开关。
|
|
6
|
+
*
|
|
7
|
+
* 键是 session:同一个 session 可以换 scope(预设重绑),scope 或能力自己的 key 任一
|
|
8
|
+
* 变化都先挂新声明、成功后再撤旧的;mount 返回 undefined 表示依赖尚未就绪,保留旧声明、
|
|
9
|
+
* 下次装配重试(旧闸比无闸安全)。不用 shared.mjs 的 sessionState:闸必须确定性撤销,
|
|
10
|
+
* 不能按容量淘汰。
|
|
11
|
+
*/
|
|
12
|
+
import { isDelegated } from './shared.mjs'
|
|
13
|
+
|
|
14
|
+
/** 受众判定:delegationDepth > 0 即子代理(与 shared.mjs 的 isDelegated 同一判据)。 */
|
|
15
|
+
export function capabilityAudience(agent) {
|
|
16
|
+
return isDelegated(agent?.session) ? 'subagent' : 'main'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 建一个按 Agent 管理的声明表。
|
|
21
|
+
*
|
|
22
|
+
* @param mount - `(agent) => { disposers, data? } | undefined`:在 agent.ctx 上声明能力并
|
|
23
|
+
* 返回撤销器;返回 undefined 表示这次没挂上(依赖未就绪),不记状态、下次 sync 重试。
|
|
24
|
+
* @returns `{ sync(agent, key), get(session), release(session), releaseAll() }`
|
|
25
|
+
*/
|
|
26
|
+
export function createAgentCapability(mount) {
|
|
27
|
+
/** session -> { scope, key, disposers, data };条件翻转或 mask 变化时先撤再挂。 */
|
|
28
|
+
const states = new Map()
|
|
29
|
+
|
|
30
|
+
const release = (session) => {
|
|
31
|
+
const state = states.get(session)
|
|
32
|
+
if (state === undefined) return
|
|
33
|
+
states.delete(session)
|
|
34
|
+
// 重挂会留下新的 effect 注册;撤销本次的,避免长会话里累积旧 state 闭包。
|
|
35
|
+
state.effectDispose?.()
|
|
36
|
+
for (const dispose of state.disposers.splice(0)) {
|
|
37
|
+
try { dispose() } catch { /* 释放失败不得反过来打断卸载 */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const releaseAll = () => { for (const session of [...states.keys()]) release(session) }
|
|
41
|
+
|
|
42
|
+
const sync = (agent, key) => {
|
|
43
|
+
const session = agent?.session
|
|
44
|
+
const scope = agent?.ctx
|
|
45
|
+
if (session === undefined || scope === undefined) return undefined
|
|
46
|
+
const existing = states.get(session)
|
|
47
|
+
if (existing !== undefined && existing.scope === scope && existing.key === key) return existing
|
|
48
|
+
// 依赖未就绪时保留旧闸:先挂新声明,成功后再撤旧的;mount 返回 undefined 直接退出。
|
|
49
|
+
const mounted = mount(agent)
|
|
50
|
+
if (mounted === undefined) return existing
|
|
51
|
+
release(session)
|
|
52
|
+
const state = { scope, key, disposers: mounted.disposers ?? [], data: mounted.data ?? {}, effectDispose: undefined }
|
|
53
|
+
states.set(session, state)
|
|
54
|
+
// 子代理与重绑都换 scope:父 mount 释放必须能撤销,scope 释放只移出自己。
|
|
55
|
+
if (typeof scope.effect === 'function') {
|
|
56
|
+
state.effectDispose = scope.effect(() => () => { if (states.get(session) === state) release(session) })
|
|
57
|
+
}
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sync,
|
|
63
|
+
get: (session) => states.get(session),
|
|
64
|
+
release,
|
|
65
|
+
releaseAll,
|
|
66
|
+
}
|
|
67
|
+
}
|
package/engine/anchor-match.mjs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* anchor-match — 关键词锚定匹配引擎(纯函数,无状态,无依赖)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 规则条件、锚定词与上下文激活
|
|
5
5
|
* 共用同一套匹配语义——主键/副键、大小写、整词、正则、组合逻辑。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* not = NOT_ALL(1):主键命中且副键全部未命中(排除);
|
|
11
|
-
* notAny = NOT_ANY(2):主键命中且至少一个副键未命中(部分排除)。
|
|
7
|
+
* 规则条件逻辑:any=主副键任一命中,all=主键命中且副键全中,
|
|
8
|
+
* not=主键命中且副键全不中,notAny=主键命中且至少一个副键未中。
|
|
9
|
+
* 上下文激活的具名副键逻辑由候选选择器组合命中数,不借用这些名称。
|
|
12
10
|
*
|
|
13
11
|
* 模式:
|
|
14
|
-
* scan =
|
|
12
|
+
* scan = 全文扫描;
|
|
15
13
|
* prefix = 文本开头匹配(custom-fallback 首轮 reasoning 锚定确认:确认词集合任一
|
|
16
14
|
* 前缀命中——ASCII 词边界前缀,非 ASCII 直 prefix)。
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
/**
|
|
17
|
+
/** 规则条件的组合逻辑。 */
|
|
20
18
|
export const MATCH_LOGIC = {
|
|
21
19
|
ANY: 'any',
|
|
22
20
|
ALL: 'all',
|
|
@@ -29,13 +27,6 @@ export function escapeRegExp(text) {
|
|
|
29
27
|
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
/** ST 正则键检测(对齐 world-info.js parseRegexFromString):
|
|
33
|
-
* 仅 /pattern/flags 显式包裹的键按正则匹配,其余一律字面(含 {{user}} 等
|
|
34
|
-
* 含花括号/正则特殊字符的普通键——ST 不做特殊字符自动检测)。 */
|
|
35
|
-
export function isRegexKey(key) {
|
|
36
|
-
return parseRegexFromString(key) !== undefined
|
|
37
|
-
}
|
|
38
|
-
|
|
39
30
|
/** 键 → RegExp(仅 /pattern/flags 形态,模式内未转义 / 分隔符视为非法);否则 undefined。 */
|
|
40
31
|
export function parseRegexFromString(key) {
|
|
41
32
|
const m = /^\/([\s\S]+?)\/([gimsuy]*)$/u.exec(key)
|
|
@@ -51,7 +42,7 @@ export function parseRegexFromString(key) {
|
|
|
51
42
|
|
|
52
43
|
/** 单键正则编译(逐键匹配:any/all/not 需要精确的命中键数,捕获组会干扰 match 计数)。
|
|
53
44
|
* useRegex 三态:true=强制正则;false=强制字面;缺省=ST 语义自动检测。 */
|
|
54
|
-
function compileSingle(key, { caseSensitive, wholeWords, useRegex,
|
|
45
|
+
function compileSingle(key, { caseSensitive, wholeWords, useRegex, wordBoundary }) {
|
|
55
46
|
const flags = caseSensitive ? '' : 'i'
|
|
56
47
|
if (useRegex === true) {
|
|
57
48
|
// ST use_regex=true:键原样作为正则(作者负责合法性)。
|
|
@@ -62,7 +53,7 @@ function compileSingle(key, { caseSensitive, wholeWords, useRegex, stWords }) {
|
|
|
62
53
|
if (auto !== undefined) return auto
|
|
63
54
|
}
|
|
64
55
|
if (wholeWords) {
|
|
65
|
-
if (
|
|
56
|
+
if (wordBoundary === 'ascii-token') return new RegExp(key.split(/\s+/).length > 1 ? escapeRegExp(key) : `(?:^|\\W)(${escapeRegExp(key)})(?:$|\\W)`, flags)
|
|
66
57
|
return new RegExp(`(^|[^\\p{L}\\p{N}])(${escapeRegExp(key)})(?![\\p{L}\\p{N}])`, `${flags}u`)
|
|
67
58
|
}
|
|
68
59
|
return new RegExp(escapeRegExp(key), flags)
|
|
@@ -86,7 +77,7 @@ function compileKeyList(list, options) {
|
|
|
86
77
|
* @param {boolean} [options.useRegex] 三态:true=强制正则 / false=强制字面 / 缺省=自动检测(ST 语义)
|
|
87
78
|
* @param {'any'|'all'|'not'|'notAny'} [options.logic] 组合逻辑(缺省 any)
|
|
88
79
|
* @param {'scan'|'prefix'} [options.mode] 匹配模式(缺省 scan)
|
|
89
|
-
* @param {
|
|
80
|
+
* @param {'ascii-token'|'unicode'} [options.wordBoundary] 整词边界策略
|
|
90
81
|
* @returns {{ scan: (text: string) => { primary: number, secondary: number, active: boolean } }}
|
|
91
82
|
*/
|
|
92
83
|
export function createAnchorMatcher(options = {}) {
|
|
@@ -98,10 +89,12 @@ export function createAnchorMatcher(options = {}) {
|
|
|
98
89
|
useRegex = undefined,
|
|
99
90
|
logic = MATCH_LOGIC.ANY,
|
|
100
91
|
mode = 'scan',
|
|
101
|
-
|
|
92
|
+
wordBoundary = 'unicode',
|
|
102
93
|
} = options
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
if (Object.hasOwn(options, 'stWords')) throw new TypeError('stWords is retired; use wordBoundary')
|
|
95
|
+
if (!['ascii-token', 'unicode'].includes(wordBoundary)) throw new TypeError('Unknown wordBoundary')
|
|
96
|
+
const primaryList = compileKeyList(keys, { caseSensitive, wholeWords, useRegex, wordBoundary })
|
|
97
|
+
const secondaryList = compileKeyList(secondaryKeys, { caseSensitive, wholeWords, useRegex, wordBoundary })
|
|
105
98
|
|
|
106
99
|
const scan = (raw) => {
|
|
107
100
|
const text = String(raw ?? '')
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 条件树 → 受众事实:规则级与动作级受众判据的唯一实现。
|
|
3
|
+
*
|
|
4
|
+
* 纯函数,只依赖纯算子清单;不 import `conditions/index.mjs`(那会经 preset 拉入宿主包)。
|
|
5
|
+
* 返回值 undefined = 无法静态收窄,由调用方按保守方向处理。
|
|
6
|
+
*/
|
|
7
|
+
import { COMPOSITE_OPERATORS } from './conditions/operators.mjs'
|
|
8
|
+
import { record } from './record.mjs'
|
|
9
|
+
|
|
10
|
+
/** 全部受众;rule-spec 的 action 回退与 audienceInfo 共用这一份。 */
|
|
11
|
+
export const AUDIENCE_ALL = Object.freeze(['main', 'subagent'])
|
|
12
|
+
|
|
13
|
+
/** 受众成员判据;与 AUDIENCE_ALL 同源,避免各处手写两个字符串比较。 */
|
|
14
|
+
export const isAudience = value => AUDIENCE_ALL.includes(value)
|
|
15
|
+
|
|
16
|
+
/** 两个受众集合求交;undefined = 无法收窄,取交集时让位于已知集合。 */
|
|
17
|
+
export function intersectAudiences(left, right) {
|
|
18
|
+
if (left === undefined) return right
|
|
19
|
+
if (right === undefined) return left
|
|
20
|
+
return new Set([...left].filter(audience => right.has(audience)))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 精确受众集合的补集;只在 not/notAny 的精确子节点上使用。 */
|
|
24
|
+
const complementAudiences = audiences => new Set(AUDIENCE_ALL.filter(audience => !audiences.has(audience)))
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 条件树 → 受众上下界;`exact` 表示结果是否能安全取补。
|
|
28
|
+
*
|
|
29
|
+
* `all` 求交时跳过无法收窄的子条件:AND 只会更窄,结果仍是「可能命中」的上界,
|
|
30
|
+
* 但不再精确——此时取补会错误排除受众,所以 `not` / `notAny` 遇非精确子节点返回未知。
|
|
31
|
+
* `any` 任一分支不精确即整体未知(保守)。
|
|
32
|
+
*/
|
|
33
|
+
function audienceInfo(tree) {
|
|
34
|
+
if (!record(tree)) return undefined
|
|
35
|
+
if (record(tree.scope)) {
|
|
36
|
+
const { audience, modelScope } = tree.scope
|
|
37
|
+
// 同节点带 pro/flash 时结果还依赖模型,不能取补;缺省与 all 只按受众收窄。
|
|
38
|
+
if (isAudience(audience)) {
|
|
39
|
+
return { audiences: new Set([audience]), exact: modelScope === undefined || modelScope === 'all' }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const operator of COMPOSITE_OPERATORS) {
|
|
43
|
+
const value = tree[operator]
|
|
44
|
+
if (value === undefined) continue
|
|
45
|
+
// 非法形状(`all: null` 之类)不猜语义:按无法收窄处理,与重构前的宿主判据一致。
|
|
46
|
+
if (operator !== 'not' && !Array.isArray(value)) return undefined
|
|
47
|
+
switch (operator) {
|
|
48
|
+
case 'all': {
|
|
49
|
+
let audiences = new Set(AUDIENCE_ALL)
|
|
50
|
+
let exact = true
|
|
51
|
+
for (const child of value) {
|
|
52
|
+
const childInfo = audienceInfo(child)
|
|
53
|
+
if (childInfo === undefined) {
|
|
54
|
+
exact = false
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
audiences = intersectAudiences(audiences, childInfo.audiences)
|
|
58
|
+
if (!childInfo.exact) exact = false
|
|
59
|
+
}
|
|
60
|
+
return { audiences, exact }
|
|
61
|
+
}
|
|
62
|
+
case 'any': {
|
|
63
|
+
const audiences = new Set()
|
|
64
|
+
for (const child of value) {
|
|
65
|
+
const childInfo = audienceInfo(child)
|
|
66
|
+
if (childInfo === undefined || !childInfo.exact) return undefined
|
|
67
|
+
for (const audience of childInfo.audiences) audiences.add(audience)
|
|
68
|
+
}
|
|
69
|
+
return { audiences, exact: true }
|
|
70
|
+
}
|
|
71
|
+
case 'not': {
|
|
72
|
+
const childInfo = audienceInfo(value)
|
|
73
|
+
if (childInfo === undefined || !childInfo.exact) return undefined
|
|
74
|
+
return { audiences: complementAudiences(childInfo.audiences), exact: true }
|
|
75
|
+
}
|
|
76
|
+
case 'notAny': {
|
|
77
|
+
const childInfo = audienceInfo({ any: value })
|
|
78
|
+
if (childInfo === undefined || !childInfo.exact) return undefined
|
|
79
|
+
return { audiences: complementAudiences(childInfo.audiences), exact: true }
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
// 新算子未声明受众语义:不收窄,交给调用方保守处理。
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 条件树 → 受众事实:这条规则可能作用于哪些受众。
|
|
91
|
+
*
|
|
92
|
+
* `undefined` = 无法静态收窄,由调用方按保守方向处理。语义与 AND/OR 一致:
|
|
93
|
+
* - `all` 求交:AND 只会更窄,无法收窄的子条件不扩大结果(结果是上界,不可取补);
|
|
94
|
+
* - `any` 保守:任一分支无法收窄,整体就无法排除另一个受众;
|
|
95
|
+
* - `not` / `notAny` 只对精确子节点取补;`scope.audience` 是唯一能精确收窄的谓词。
|
|
96
|
+
*
|
|
97
|
+
* 算子集合来自 `COMPOSITE_OPERATORS`(与 `compileWhen` / `composite` 同源);
|
|
98
|
+
* 新增算子若没补受众语义,`test/engine/rule-audiences.test.mjs` 的映射表先红。
|
|
99
|
+
*/
|
|
100
|
+
export function audienceOf(tree) {
|
|
101
|
+
return audienceInfo(tree)?.audiences
|
|
102
|
+
}
|
package/engine/branch.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 分支语法的唯一展开器:只认 `if`/`then`/`else`,旧名 `when`/`do` 由各入口显式拒绝。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 严格版服务规则编译(`rule-spec.mjs`);宽松版 `expandActionsLenient` 只服务展示投影
|
|
5
|
+
* (`rule-facts.mjs`),两者共用 `isBranchNode` / `conjunction` 的分支语义。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { record } from './record.mjs'
|
|
9
9
|
|
|
10
10
|
/** 分支节点:占动作位、没有 `kind`,但带 `if`/`then`/`else` 之一。 */
|
|
11
11
|
export const isBranchNode = node => record(node) && node.kind === undefined
|
|
@@ -17,28 +17,71 @@ export function conjunction(conditions) {
|
|
|
17
17
|
return conditions.length === 1 ? conditions[0] : { all: [...conditions] }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** 分支两支的 outer 条件:then 追加 if,else 追加 not(if);缺 if 时保留父条件。 */
|
|
21
|
+
const branchOuters = (node, outer) => ({
|
|
22
|
+
then: node.if === undefined ? outer : [...outer, node.if],
|
|
23
|
+
else: node.if === undefined ? outer : [...outer, { not: node.if }],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function walkActions(list, { outer = [], bypass = false, lenient = false, label } = {}) {
|
|
27
|
+
if (!Array.isArray(list)) {
|
|
28
|
+
if (!lenient) throw new TypeError(`${label} must be an array`)
|
|
29
|
+
return []
|
|
30
|
+
}
|
|
31
|
+
const out = []
|
|
32
|
+
for (const [index, node] of list.entries()) {
|
|
33
|
+
const at = `${label}[${index}]`
|
|
34
|
+
if (isBranchNode(node)) {
|
|
35
|
+
if (!lenient) {
|
|
36
|
+
if (node.if === undefined) throw new TypeError(`${at}: a branch needs if`)
|
|
37
|
+
if (node.then === undefined) throw new TypeError(`${at}: a branch needs then`)
|
|
38
|
+
}
|
|
39
|
+
const { then: thenOuter, else: elseOuter } = branchOuters(node, outer)
|
|
40
|
+
const thenList = lenient && !Array.isArray(node.then) ? undefined : node.then
|
|
41
|
+
if (thenList !== undefined) {
|
|
42
|
+
out.push(...walkActions(thenList, {
|
|
43
|
+
outer: thenOuter, bypass, lenient,
|
|
44
|
+
...(lenient ? {} : { label: `${at}.then` }),
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
const elseList = lenient && !Array.isArray(node.else) ? undefined : node.else
|
|
48
|
+
if (elseList !== undefined) {
|
|
49
|
+
out.push(...walkActions(elseList, {
|
|
50
|
+
outer: elseOuter, bypass, lenient,
|
|
51
|
+
...(lenient ? {} : { label: `${at}.else` }),
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (lenient) {
|
|
57
|
+
if (record(node) && typeof node.kind === 'string') out.push({ node, conditions: outer, bypass })
|
|
58
|
+
} else {
|
|
59
|
+
out.push({ node, conditions: outer, bypass })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
20
65
|
/**
|
|
21
66
|
* 把一份动作声明展开为扁平的「动作 + 自身分支条件」列表。
|
|
22
67
|
*
|
|
23
68
|
* - 普通动作原样带出,分支条件 = `outer` 的合取(空 = 无条件);
|
|
24
69
|
* - 分支节点的 `then` 追加本层 `if`、`else` 追加 `not(if)`——互斥由结构保证,不由作者手写;
|
|
70
|
+
* 有状态谓词(phase / count)还须同时收到 `session/event` 观察(`trigger.mjs` 的
|
|
71
|
+
* `wireTriggerObservers`)才成立,否则状态停在冷扫那一刻、互斥也就名存实亡;
|
|
25
72
|
* - 返回顺序即声明序(then 在前、else 在后);
|
|
26
73
|
* - `bypass` 标记该动作来自**声明级** `else`:它的条件已含 `not(if)`,不能再叠加声明级判定。
|
|
27
74
|
*/
|
|
28
75
|
export function expandActions(list, options = {}, label) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
out.push({ node, conditions: outer, bypass })
|
|
42
|
-
}
|
|
43
|
-
return out
|
|
76
|
+
return walkActions(list, { ...options, lenient: false, label })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 宽松展开:只服务展示投影(`rule-facts.mjs`)。
|
|
81
|
+
*
|
|
82
|
+
* 与 `expandActions` 共享同一条 then/else 遍历,但遇到未完成的分支(缺 `if`/`then`)不抛错:
|
|
83
|
+
* 有 `then`/`else` 数组就继续下钻;缺 `if` 时两支都按父条件展开(保守,不伪造否定)。
|
|
84
|
+
*/
|
|
85
|
+
export function expandActionsLenient(list, options = {}) {
|
|
86
|
+
return walkActions(list, { ...options, lenient: true })
|
|
44
87
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `compaction-epoch.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
|
|
2
|
+
|
|
3
|
+
/** epoch 感知的晋升相位追踪器;仅覆盖插件依赖面(宿主只把它交给 pre-step 批执行)。 */
|
|
4
|
+
export interface PromotionTracker {
|
|
5
|
+
/**
|
|
6
|
+
* 该 agent 当前相位;`boundary` 是最后一次成功压缩的 seq(压缩前为 -1),
|
|
7
|
+
* `promoted` 表示该边界之后存在 durable 晋升信号。
|
|
8
|
+
*/
|
|
9
|
+
status(agent?: unknown): { boundary: number; promoted: boolean }
|
|
10
|
+
/** 增量喂入 `session/event`。 */
|
|
11
|
+
observe(session: unknown, event: unknown): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EpochPromotionOptions {
|
|
15
|
+
includeSubagents?: boolean
|
|
16
|
+
promoteGate?: boolean
|
|
17
|
+
promoteAfterFirstResponse?: boolean
|
|
18
|
+
maxPromoteSteps?: number
|
|
19
|
+
reasoningPattern?: string
|
|
20
|
+
reasoningNegativePattern?: string
|
|
21
|
+
reasoningFlags?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 建一份相位追踪器;`promoteEvents` 是 durable 事件类型集合(见 `shared.mjs#PROMOTE_EVENTS`)。 */
|
|
25
|
+
export function createEpochPromotion(promoteEvents: Iterable<string>, options?: EpochPromotionOptions): PromotionTracker
|