dsh-plugin-prompt-tool 3.0.2 → 3.1.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 +17 -1
- package/engine/actions/index.mjs +5 -3
- package/engine/actions/tool-visibility.mjs +4 -1
- package/engine/compositions/source/local/context-provenance.yml +9 -0
- package/engine/compositions/source/local/generation-recovery.yml +10 -0
- package/engine/compositions/source/local/retrieval.yml +7 -0
- package/engine/compositions/source/local/soul-ledger.yml +3 -0
- package/engine/compositions/source/local/soul-maintenance.yml +9 -0
- package/engine/conditions/examples.mjs +1 -0
- package/engine/conditions/index.mjs +7 -0
- package/engine/conditions/manifest.mjs +28 -0
- package/engine/context-idle-compaction.mjs +58 -0
- package/engine/context-provenance.d.mts +7 -0
- package/engine/context-provenance.mjs +156 -0
- package/engine/context-pruner.mjs +64 -0
- package/engine/context-routing.mjs +45 -0
- package/engine/context-summary.mjs +80 -0
- package/engine/fillers.mjs +11 -2
- package/engine/generation-recovery-coordinator.mjs +40 -0
- package/engine/generation-recovery-maintenance.mjs +18 -0
- package/engine/generation-recovery.mjs +153 -0
- package/engine/host-package.mjs +13 -0
- package/engine/layers.d.mts +4 -0
- package/engine/layers.mjs +57 -20
- package/engine/present-as.mjs +2 -2
- package/engine/retrieval-reference.d.mts +5 -0
- package/engine/retrieval-reference.mjs +26 -0
- package/engine/retrieval.mjs +17 -0
- package/engine/rule-engine.mjs +1 -0
- package/engine/rule-runtime.d.mts +8 -1
- package/engine/rule-runtime.mjs +18 -10
- package/engine/rule-spec.mjs +4 -2
- package/engine/schema.mjs +1 -1
- package/engine/service-names.d.mts +5 -0
- package/engine/service-names.mjs +5 -0
- package/engine/soul-ledger.mjs +8 -0
- package/engine/soul-maintenance.mjs +20 -0
- package/lib/client.js +12 -2
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +1868 -208
- package/modules/soul/assets/ledger.md +6 -2
- package/modules/soul/assets/manifest.yml +2 -0
- package/modules/soul/assets/templates/agents-template.md +3 -0
- package/modules/soul/assets/templates/self-template.md +19 -0
- package/modules/soul/assets/tools.md +10 -0
- package/modules/soul/module.yml +89 -15
- package/modules/tool-surface/module.yml +28 -6
- package/package.json +1 -1
- package/skills/dsh-module/reference.md +1 -1
- package/templates/80-tool-surface.yml +25 -4
package/README.md
CHANGED
|
@@ -67,6 +67,8 @@ rules:
|
|
|
67
67
|
|
|
68
68
|
规则是唯一行为源;共享模型与能力参数保存于 layerSettings,人设、变量与自定义工具各有独立顶层段。根 [module.yml](module.yml) 从权威目录生成九层及27个共享参数参考,示例默认关闭。
|
|
69
69
|
|
|
70
|
+
单个模块装配失败时,首次加载跳过它,更新时恢复它的上次有效贡献;其他正常模块继续加载和更新。晚到服务的初始化失败也按此处理,等待中的新版本不会覆盖最后成功版本。故障仍会报告,修正该模块定义或显式刷新后重试。全局配置和跨模块独占冲突仍明确拒绝,不自动选择赢家;完整边界见[运行时装配](docs/engine-reuse.md)。
|
|
71
|
+
|
|
70
72
|
- 条件使用 if,动作用 then,另有 else;动作身份在完整规则树中唯一。
|
|
71
73
|
- 注入正文策略为 static、placeholder、context-activation;文本与长度分类、确认、工具名和受众由通用条件承担,正文、投递、请求与流控制由动作组合。
|
|
72
74
|
- 工具提示通过 tool-catalog 只读引用同规则工具可见面声明,自动列出常驻名单与发现工具;提示正文和投递由模块规则控制。
|
|
@@ -110,6 +112,8 @@ persona:
|
|
|
110
112
|
| module-file-tools | module_file_read / module_file_append |
|
|
111
113
|
| module-manifest-tools | module_manifest |
|
|
112
114
|
| module-import-tools | module_import |
|
|
115
|
+
| soul-ledger | soul_ledger |
|
|
116
|
+
| retrieval | module_search / module_open |
|
|
113
117
|
| tool-config-engine | customTools 中的实际工具 |
|
|
114
118
|
|
|
115
119
|
完整规则工具先读原始树与版本,再提交合法完整候选;删除整规则需显式 rule:null。变量工具提供 global/module 的 list/get/set/delete,set 必须显式字符串。模块私有文件(memory.md、assets/ 只读、data/ 只追加)由 module_file_read/append 读写,不自动注入;module_manifest 按 assets/manifest.yml 做 next/check/apply,只写 data/、不覆盖。
|
|
@@ -124,7 +128,19 @@ ST JSON 预设、PNG/JSON 角色卡和独立世界书,经统一预览/确认/
|
|
|
124
128
|
|
|
125
129
|
outlet/vectorized/triggers 不能等价执行,相关规则禁用并报告;角色过滤、自动化、深度插入、预算、群聊和脚本都有明确限制。完整映射及报告边界见 [SillyTavern 导入](docs/SillyTavern.md)。
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
模块导出分享完整定义和允许的附件,不分享全局变量、私有 memory.md 或 data/。旧私有记忆标记必须显式处理,不靠删除标记继续导出。路径、链接、体积、覆盖和备份边界见 [资产交换](docs/asset-transfer.md)。
|
|
132
|
+
|
|
133
|
+
书房数据通过 `/prompt-tool data-import <JSON>` 显式导入。先指定 `moduleId`、来源书房根 `sourceDirectory` 和相对文件列表 `paths`,预览创建、跳过与冲突项;确认后在相同 JSON 中加上返回的 `expectedRevision`(值取预览的 `revision`)应用。只补缺失文件,冲突项保留目标,源文件不动;旧 `02-墨.jsonl` 映射到 `data/02-soul.jsonl`。新建书房用 `{"moduleId":"soul","initialize":true}` 预览模块自己的清单,也须带预览版本应用。完整例子与限制见 [数据导入](docs/architecture-params.md#书房数据预览与导入)。
|
|
134
|
+
|
|
135
|
+
Soul 模块包含结构化账本、授权文件/历史检索、每步动态 SELF、上下文压缩与裁剪溯源、生成恢复和维护心跳。SELF 由 `rule-engine` 的内置动态内容源提供(`strategy: placeholder`、`fill: soul-self`),无需模块附带策略 JS。先用上述初始化预览补齐 `SELF.src.md` 骨架;已有模块副本须显式更新,包升级不会覆盖用户定义或私有数据,旧副本仍可使用自身保留的自定义策略。
|
|
136
|
+
|
|
137
|
+
`soul_ledger` 自动署名真实会话,`lead` 需宿主审批,只有最后主导会话可追加 `promise`;通用文件追加拒绝新旧账本名。`module_search` 返回的引用交给 `module_open` 回取,历史会话只读取声明的白名单。SELF 的当前身份在每步读取时加入,维护默认每分钟只更新索引与快照,`wake` 独立且默认关闭。
|
|
138
|
+
|
|
139
|
+
`generation-recovery` 的通用默认只截断;Soul 显式启用空回合检测和有界恢复,主会话额度 3、子代理 1,每会话独立且重挂不清零。维护压缩和扩大范围默认关闭。配置与边界见 [引擎运行时](docs/engine-reuse.md#重复输出安全截断)、[账本与SELF](docs/architecture-params.md#soul账本与派生快照)。
|
|
140
|
+
|
|
141
|
+
这些运行能力由本插件实现,不需要源项目 `dsh-agent-soul` 的仓库、插件或后台服务;上下文治理仍要求宿主提供[官方能力包](docs/engine-reuse.md#上下文运行时与溯源)(按包名白名单从 DSH 自带包解析,运行期不比对版本)。本模块不等价于源项目的全部功能:检索采用授权范围内的字面匹配,未提供源近义检索、上下文水位提醒、完整 doctor/诊断工具或附属面板;恢复判据与额度策略也有差异。`assets/reference/` 保留的源文档用于背景参考,其中的源 CLI 和第三方工具不表示本插件已提供对应能力。
|
|
142
|
+
|
|
143
|
+
上下文能力按包名白名单从 DSH 自带包解析,不增加插件依赖,运行期不校验版本;仅包缺失或未登记时拒绝该能力装配。包代码更新需要用户重启 DSH 加载;加载后修改模块启停/参数沿运行时刷新,不改官方 preset 或 profile patch。
|
|
128
144
|
|
|
129
145
|
## 升级与旧格式
|
|
130
146
|
|
package/engine/actions/index.mjs
CHANGED
|
@@ -83,9 +83,7 @@ export function prepareAction(action, options = {}) {
|
|
|
83
83
|
const on = withWhen(options.on ?? channelBinder(ctx, kind, plugin, options.prepend === true), options.when, warnOnce, phase, missAll)
|
|
84
84
|
// 每轮预算的轮边界由 durable 事件驱动(与谓词的 observe 同一事件源,但**独立记账**:
|
|
85
85
|
// 预算是「本动作生效次数」,不是 durable 事件计数)。
|
|
86
|
-
if (budget !== undefined) collect(ctx.on('session/event', (session, event) => budget.observe(session, event)))
|
|
87
86
|
const take = (subject) => budget?.take(subject) ?? true
|
|
88
|
-
bind?.(ctx, { warnOnce, on, collect, take, onMiss, agent: options.agent })
|
|
89
87
|
const dispose = () => {
|
|
90
88
|
for (const release of disposers.splice(0)) {
|
|
91
89
|
try {
|
|
@@ -95,7 +93,11 @@ export function prepareAction(action, options = {}) {
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
|
-
|
|
96
|
+
try {
|
|
97
|
+
if (budget !== undefined) collect(ctx.on('session/event', (session, event) => budget.observe(session, event)))
|
|
98
|
+
bind?.(ctx, { warnOnce, on, collect, take, onMiss, agent: options.agent })
|
|
99
|
+
keepDisposer(ctx, dispose, `${plugin}: action ${labelOf(action)}`)
|
|
100
|
+
} catch (error) { dispose(); throw error }
|
|
99
101
|
// 调用方(rule-runtime)把 miss 挂到该动作的条目上;没有回调时保持 undefined。
|
|
100
102
|
dispose.miss = missHandlers.length === 0 ? undefined : missAll
|
|
101
103
|
return dispose
|
|
@@ -139,7 +139,10 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
139
139
|
if (toolName === RUN_CODE || typeof toolName !== 'string' || toolName.length === 0) return undefined
|
|
140
140
|
if (deny?.has(toolName)) return `${label}: tool ${JSON.stringify(toolName)} is denied by tool-visibility`
|
|
141
141
|
if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName, state.unlocked)) {
|
|
142
|
-
|
|
142
|
+
const hint = discovery === undefined
|
|
143
|
+
? 'the allow list is the only path; this action has no discovery source'
|
|
144
|
+
: `unlock it with tool_search({"toolNames":[${JSON.stringify(toolName)}]}); the gate allows unlocked tools from the next request on`
|
|
145
|
+
return `${label}: tool ${JSON.stringify(toolName)} is not allowed by tool-visibility; ${hint}`
|
|
143
146
|
}
|
|
144
147
|
return undefined
|
|
145
148
|
}))
|
|
@@ -8,6 +8,7 @@ import { createPresetPredicate } from './preset.mjs'
|
|
|
8
8
|
import { createScopePredicate } from './scope.mjs'
|
|
9
9
|
import { createAnchorPredicate } from './anchor.mjs'
|
|
10
10
|
import { createVariablePredicate } from './variable.mjs'
|
|
11
|
+
import { createManifestPredicate } from './manifest.mjs'
|
|
11
12
|
import { withAvailableInput } from './availability.mjs'
|
|
12
13
|
import { composite } from './composite.mjs'
|
|
13
14
|
import { COMPOSITE_OPERATORS } from './operators.mjs'
|
|
@@ -21,6 +22,7 @@ export * from './preset.mjs'
|
|
|
21
22
|
export * from './scope.mjs'
|
|
22
23
|
export * from './anchor.mjs'
|
|
23
24
|
export * from './variable.mjs'
|
|
25
|
+
export * from './manifest.mjs'
|
|
24
26
|
export { subjectOf, channelTextSubjects, channelFactSubjects, FACT_PREDICATE_SUBJECTS } from './subject.mjs'
|
|
25
27
|
export { composite } from './composite.mjs'
|
|
26
28
|
export const PREDICATE_FACTORIES = Object.freeze({
|
|
@@ -34,6 +36,7 @@ export const PREDICATE_FACTORIES = Object.freeze({
|
|
|
34
36
|
scope: createScopePredicate,
|
|
35
37
|
anchor: createAnchorPredicate,
|
|
36
38
|
variable: createVariablePredicate,
|
|
39
|
+
manifest: createManifestPredicate,
|
|
37
40
|
})
|
|
38
41
|
export { COMPOSITE_OPERATORS }
|
|
39
42
|
|
|
@@ -95,5 +98,9 @@ export function compileWhen(node, context = {}) {
|
|
|
95
98
|
if (key === 'variable') {
|
|
96
99
|
return createVariablePredicate(options, { store: context.variableStore, moduleId: context.moduleId })
|
|
97
100
|
}
|
|
101
|
+
// manifest 谓词同样自带三值:读取器未绑定或清单不可读时为 UNAVAILABLE。
|
|
102
|
+
if (key === 'manifest') {
|
|
103
|
+
return createManifestPredicate(options, { manifestCheck: context.manifestCheck, moduleId: context.moduleId })
|
|
104
|
+
}
|
|
98
105
|
return withAvailableInput(key, options, factory(options))
|
|
99
106
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manifest 谓词:规则读取来源模块 `assets/manifest.yml` 的 checks 事实。
|
|
3
|
+
*
|
|
4
|
+
* 读取器由宿主装配经 options 在挂载期绑定,结果缓存 1000ms,清单戳变化提前失效;
|
|
5
|
+
* 读取器缺失、清单缺失/损坏都判 UNAVAILABLE——不能经 not 反转,也不能当「检查失败」。
|
|
6
|
+
*/
|
|
7
|
+
import { UNAVAILABLE } from './availability.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ ok?: boolean }} options
|
|
11
|
+
* @param {{ manifestCheck?: { read: (moduleId: string) => boolean | undefined }, moduleId?: string }} [context]
|
|
12
|
+
*/
|
|
13
|
+
export function createManifestPredicate(options = {}, context = {}) {
|
|
14
|
+
const unknown = Object.keys(options).filter((key) => key !== 'ok')
|
|
15
|
+
if (unknown.length > 0) throw new TypeError(`predicates: manifest does not take ${unknown.join(', ')}`)
|
|
16
|
+
if (typeof options.ok !== 'boolean') throw new TypeError('predicates: manifest ok must be a boolean')
|
|
17
|
+
const expected = options.ok
|
|
18
|
+
const reader = context.manifestCheck
|
|
19
|
+
const moduleId = context.moduleId
|
|
20
|
+
const predicate = () => {
|
|
21
|
+
if (reader === undefined || typeof reader.read !== 'function') return UNAVAILABLE
|
|
22
|
+
if (typeof moduleId !== 'string' || moduleId.length === 0) return UNAVAILABLE
|
|
23
|
+
const observed = reader.read(moduleId)
|
|
24
|
+
return typeof observed === 'boolean' ? observed === expected : UNAVAILABLE
|
|
25
|
+
}
|
|
26
|
+
predicate.kind = 'manifest'
|
|
27
|
+
return predicate
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
3
|
+
import { importHostCapability } from './host-package.mjs'
|
|
4
|
+
import { frameSummary } from './context-summary.mjs'
|
|
5
|
+
import { historyEvents } from './history.mjs'
|
|
6
|
+
|
|
7
|
+
/** 已领取idle维护窗口内的局部事务;Basic.compactRegion仅允许打开的turn。 */
|
|
8
|
+
export async function compactIdleRegion({ agent, range, signal, meter, sessions, compaction, pairing }) {
|
|
9
|
+
signal.throwIfAborted()
|
|
10
|
+
const { createUserMessage, errorChain } = await importHostCapability('@deepseek-ai/dsh-llm')
|
|
11
|
+
signal.throwIfAborted()
|
|
12
|
+
const session = agent.session, nodes = session.surface.nodes
|
|
13
|
+
const start = nodes.indexOf(range.start), end = nodes.indexOf(range.end)
|
|
14
|
+
if (start < 0 || end < start || !pairing.toolPairingBalancedBefore(session, range.start)
|
|
15
|
+
|| !pairing.toolPairingBalancedAfter(session, range.end)) throw new Error('idle compaction: invalid balanced range')
|
|
16
|
+
const history = historyEvents(session), bySeq = new Map(history.map(event => [event.seq, event]))
|
|
17
|
+
const turn = history.findLast(event => event.type === 'turn/start' || event.type === 'turn/end')
|
|
18
|
+
if (turn?.type === 'turn/start') throw new Error('idle compaction: turn is open')
|
|
19
|
+
const lock = history.findLast(event => ['compaction/start', 'compaction/end', 'session/end-seed'].includes(event.type))
|
|
20
|
+
if (lock?.type === 'compaction/start') throw new Error('idle compaction: already running')
|
|
21
|
+
const seqs = nodes.slice(start, end + 1)
|
|
22
|
+
const selected = meter.measure(session).nodes.slice(start, end + 1)
|
|
23
|
+
if (!isDeepStrictEqual(selected.map(n => n.seq), seqs)) throw new Error('idle compaction: stale pricing')
|
|
24
|
+
const original = seqs.map(seq => bySeq.get(seq))
|
|
25
|
+
if (original.some(event => !event)) throw new Error('idle compaction: source event missing')
|
|
26
|
+
if (original.some(event => event.type === 'system/message')) throw new Error('idle compaction: system head cannot be replaced')
|
|
27
|
+
const messages = original.map(event => session.deriveEventMessage(event)).filter(Boolean)
|
|
28
|
+
const head = bySeq.get(nodes[0])
|
|
29
|
+
if (head?.type === 'system/message') messages.unshift(session.deriveEventMessage(head))
|
|
30
|
+
const header = session.requestHeader()
|
|
31
|
+
const lifecycle = { compactionId: randomUUID(), turn: null }
|
|
32
|
+
const startEvent = session.append('compaction/start', lifecycle)
|
|
33
|
+
let closing = false
|
|
34
|
+
try {
|
|
35
|
+
const summary = await compaction.summarize({ messages, ...(header?.tools ? { tools: header.tools } : {}) }, agent, signal)
|
|
36
|
+
signal.throwIfAborted()
|
|
37
|
+
const current = session.surface.nodes, first = current.indexOf(range.start)
|
|
38
|
+
const currentEvents = new Map(historyEvents(session).map(event => [event.seq, event]))
|
|
39
|
+
if (!isDeepStrictEqual(current.slice(first, first + seqs.length), seqs)
|
|
40
|
+
|| !isDeepStrictEqual(seqs.map(seq => currentEvents.get(seq)), original)
|
|
41
|
+
|| !isDeepStrictEqual(meter.measure(session).nodes.slice(first, first + seqs.length), selected)) throw new Error('idle compaction: selected surface changed')
|
|
42
|
+
const checkpoint = createUserMessage({ content: frameSummary(summary.summary), source: pairing.compactCheckpointSource(lifecycle.compactionId) })
|
|
43
|
+
if (meter.estimateMessage(checkpoint) >= selected.reduce((sum, node) => sum + node.tokens, 0)) throw new Error('idle compaction: summary is not smaller')
|
|
44
|
+
const shadowedTokenCount = selected.reduce((sum, node) => sum + node.heuristicTokens, 0)
|
|
45
|
+
const summaryEvent = session.append('compaction/summary', { ...summary, compactionId: lifecycle.compactionId,
|
|
46
|
+
shadowedRange: range, shadowedSeqs: seqs, shadowedTokenCount })
|
|
47
|
+
signal.throwIfAborted()
|
|
48
|
+
session.append('user/message', checkpoint, { surfaceOp: { op: 'replace', startSeq: range.start, endSeq: range.end }, sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...seqs] })
|
|
49
|
+
signal.throwIfAborted()
|
|
50
|
+
closing = true
|
|
51
|
+
const endEvent = session.append('compaction/end', lifecycle)
|
|
52
|
+
return { compactionId: lifecycle.compactionId, startSeq: startEvent.seq, summarySeq: summaryEvent.seq, endSeq: endEvent.seq,
|
|
53
|
+
summary: summary.summary, shadowedRange: range, shadowedSeqs: seqs, shadowedTokenCount }
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!closing) { closing = true; session.append('compaction/end', { ...lifecycle, error: errorChain(error) }) }
|
|
56
|
+
throw error
|
|
57
|
+
} finally { await sessions.flush(session) }
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ContextRuntime {
|
|
2
|
+
compactNow(signal?: AbortSignal): Promise<unknown>
|
|
3
|
+
compactRegion(start: number, end: number, signal?: AbortSignal): Promise<unknown>
|
|
4
|
+
compactRecovery(signal?: AbortSignal): Promise<unknown>
|
|
5
|
+
status(): { state: string; last: unknown }
|
|
6
|
+
}
|
|
7
|
+
export function contextRuntimeOf(agent: object): ContextRuntime | undefined
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { bool, defineConfig, enumOf, int, passthrough } from './fields.mjs'
|
|
3
|
+
import { capabilityAudience } from './agent-capability.mjs'
|
|
4
|
+
import { importHostCapability } from './host-package.mjs'
|
|
5
|
+
import { routeContextRuntime } from './context-routing.mjs'
|
|
6
|
+
import { createProvenancePruner } from './context-pruner.mjs'
|
|
7
|
+
import { createProvenanceCompaction } from './context-summary.mjs'
|
|
8
|
+
import { compactIdleRegion } from './context-idle-compaction.mjs'
|
|
9
|
+
import { historyEvents } from './history.mjs'
|
|
10
|
+
|
|
11
|
+
export const name = 'context-provenance'
|
|
12
|
+
export const inject = ['llm', 'sessions', 'tokenMeter']
|
|
13
|
+
const configObject = passthrough((value, plugin, field) => {
|
|
14
|
+
if (value === undefined) return {}
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${plugin}: ${field} must be an object`)
|
|
16
|
+
return value
|
|
17
|
+
})
|
|
18
|
+
export const configContract = defineConfig({
|
|
19
|
+
enabled: bool({ default: false }), audience: enumOf(['all', 'main', 'subagent'], { default: 'all' }),
|
|
20
|
+
auto: bool({ default: true }), modelSwitch: bool({ default: true }), prune: bool({ default: true }),
|
|
21
|
+
basic: configObject, pruner: configObject,
|
|
22
|
+
recoveryRetainTokens: int({ min: 0, default: 1024 }),
|
|
23
|
+
})
|
|
24
|
+
const runtimes = new WeakMap()
|
|
25
|
+
export const contextRuntimeOf = agent => runtimes.get(agent)
|
|
26
|
+
|
|
27
|
+
/** 用真实surface位置与配对边界选区;保留尾部,不把seq当连续整数。 */
|
|
28
|
+
export function compactableRange(session, measurement, retainTokens, pairing) {
|
|
29
|
+
const nodes = measurement.nodes
|
|
30
|
+
const events = new Map(historyEvents(session).map(event => [event.seq, event]))
|
|
31
|
+
if (nodes.some(node => !events.has(node.seq))) throw new Error('context-provenance: surface event missing')
|
|
32
|
+
let first = nodes.findIndex(node => events.get(node.seq).type !== 'system/message')
|
|
33
|
+
if (first < 0) return null
|
|
34
|
+
while (first < nodes.length && !pairing.toolPairingBalancedBefore(session, nodes[first].seq)) first++
|
|
35
|
+
let retained = 0, last = nodes.length - 1
|
|
36
|
+
while (last >= first && retained < retainTokens) retained += nodes[last--].tokens
|
|
37
|
+
while (last >= first && !pairing.toolPairingBalancedAfter(session, nodes[last].seq)) last--
|
|
38
|
+
return last < first ? null : { start: nodes[first].seq, end: nodes[last].seq }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 宿主依赖是实际适配器接缝,隔离验证可提供同版本官方构造器。 */
|
|
42
|
+
export async function mountContextRuntime(ctx, raw, { agent, sourceModuleId }, packages) {
|
|
43
|
+
const config = configContract.parse(raw, name)
|
|
44
|
+
if (!config.enabled || !agent || config.audience !== 'all' && config.audience !== capabilityAudience(agent)) return
|
|
45
|
+
if (runtimes.has(agent)) throw new Error('context-provenance: Agent already has a runtime owner')
|
|
46
|
+
ctx.fiber.assertActive()
|
|
47
|
+
const { Basic, Pruner, pairing } = packages
|
|
48
|
+
const privateCtx = new Context()
|
|
49
|
+
const lifetime = new AbortController()
|
|
50
|
+
let active = true
|
|
51
|
+
const status = { state: 'ready', last: null }
|
|
52
|
+
const off = []
|
|
53
|
+
let offRoute = () => {}, runtime
|
|
54
|
+
// 先登记所有权再等待构造器,父scope在await期间释放也能取消私有实例。
|
|
55
|
+
const dispose = ctx.effect(() => () => {
|
|
56
|
+
active = false; lifetime.abort(); status.state = 'disabled'
|
|
57
|
+
if (runtime && runtimes.get(agent) === runtime) runtimes.delete(agent)
|
|
58
|
+
offRoute(); for (const remove of off) remove()
|
|
59
|
+
return privateCtx.fiber.dispose()
|
|
60
|
+
})
|
|
61
|
+
const meter = ctx.get('tokenMeter'), llm = ctx.get('llm'), sessions = ctx.get('sessions')
|
|
62
|
+
// 只把公开方法绑定到原服务;私有Context不接管共享服务生命周期。
|
|
63
|
+
privateCtx.provide('tokenMeter', { measure: (...args) => meter.measure(...args), estimateMessage: message => meter.estimateMessage(message) })
|
|
64
|
+
privateCtx.provide('llm', { stream: options => llm.stream(options), resolveModelInfo: (...args) => llm.resolveModelInfo(...args) })
|
|
65
|
+
privateCtx.provide('sessions', { flush: session => sessions.flush(session) })
|
|
66
|
+
const combined = signal => signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal
|
|
67
|
+
const check = signal => { combined(signal).throwIfAborted() }
|
|
68
|
+
const Compaction = createProvenanceCompaction(Basic, { sourceModuleId, isActive: () => active, signalOf: () => lifetime.signal })
|
|
69
|
+
try {
|
|
70
|
+
if (config.prune) {
|
|
71
|
+
const CitedPruner = createProvenancePruner(Pruner, { sourceModuleId, isActive: () => active, signalOf: () => lifetime.signal })
|
|
72
|
+
await privateCtx.plugin(CitedPruner, config.pruner)
|
|
73
|
+
check()
|
|
74
|
+
}
|
|
75
|
+
await privateCtx.plugin(Compaction, { ...config.basic, auto: config.auto })
|
|
76
|
+
check()
|
|
77
|
+
} catch (error) { await dispose(); throw error }
|
|
78
|
+
const compaction = privateCtx.get('compaction'), pruner = privateCtx.get('toolResultPruner')
|
|
79
|
+
const run = async (action, signal) => {
|
|
80
|
+
check(signal); status.state = 'running'
|
|
81
|
+
try {
|
|
82
|
+
const result = await action(combined(signal))
|
|
83
|
+
check(signal)
|
|
84
|
+
status.state = result === null ? 'no-op' : 'success'; status.last = result
|
|
85
|
+
return result
|
|
86
|
+
} catch (error) { status.state = combined(signal).aborted ? 'cancelled' : 'failed'; throw error }
|
|
87
|
+
}
|
|
88
|
+
runtime = {
|
|
89
|
+
status: () => ({ ...status }),
|
|
90
|
+
compactNow: (signal, commandId) => run(abort => compaction.compactNow(agent, abort, commandId), signal),
|
|
91
|
+
compactRegion: (start, end, signal) => run(abort => agent.status === 'idle'
|
|
92
|
+
? agent.runMaintenance(maintenanceSignal => compactIdleRegion({ agent, range: { start, end },
|
|
93
|
+
signal: AbortSignal.any([abort, maintenanceSignal]), meter, sessions, compaction, pairing }))
|
|
94
|
+
: compaction.compactRegion(start, end, agent, abort), signal),
|
|
95
|
+
compactRecovery: signal => run(abort => agent.runMaintenance(async maintenanceSignal => {
|
|
96
|
+
const operation = AbortSignal.any([abort, maintenanceSignal])
|
|
97
|
+
operation.throwIfAborted()
|
|
98
|
+
const range = compactableRange(agent.session, meter.measure(agent.session), config.recoveryRetainTokens, pairing)
|
|
99
|
+
if (!range) return null
|
|
100
|
+
return compactIdleRegion({ agent, range, signal: operation, meter, sessions, compaction, pairing })
|
|
101
|
+
}), signal),
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
runtimes.set(agent, runtime)
|
|
105
|
+
offRoute = routeContextRuntime(ctx, agent, {
|
|
106
|
+
compaction: {
|
|
107
|
+
compactNow: (_owner, signal, commandId) => runtime.compactNow(signal, commandId),
|
|
108
|
+
compactRegion: (start, end, _owner, signal) => runtime.compactRegion(start, end, signal),
|
|
109
|
+
compactIfNeeded: (_owner, trigger, signal) => run(abort => compaction.compactIfNeeded(agent, trigger, abort), signal),
|
|
110
|
+
},
|
|
111
|
+
toolResultPruner: pruner ? { pruneSession: session => { check(); return pruner.pruneSession(session, lifetime.signal) } } : undefined,
|
|
112
|
+
})
|
|
113
|
+
for (const event of ['agent/pre-step', 'agent/request-error']) off.push(ctx.on(event, async (payload, next) => {
|
|
114
|
+
if (!active || payload.agent !== agent) return next()
|
|
115
|
+
const result = await privateCtx.waterfall(event, { ...payload, signal: combined(payload.signal) }, () => undefined)
|
|
116
|
+
return result === undefined ? next() : result
|
|
117
|
+
}, { prepend: true }))
|
|
118
|
+
// Basic的retry账本按官方durable结算与idle复位。
|
|
119
|
+
off.push(ctx.on('session/event', (session, event) => { if (active && session === agent.session) privateCtx.emit('session/event', session, event) }))
|
|
120
|
+
off.push(ctx.on('agent/status', payload => { if (active && payload.agent === agent) privateCtx.emit('agent/status', payload) }))
|
|
121
|
+
if (config.modelSwitch) off.push(ctx.on('agent/request', async (payload, next) => {
|
|
122
|
+
const request = await next()
|
|
123
|
+
if (!active || payload.agent !== agent || payload.signal?.aborted) return request
|
|
124
|
+
const previous = agent.session.requestHeader()
|
|
125
|
+
if (!previous || previous.config.provider === request.provider && previous.config.model === request.model) return request
|
|
126
|
+
const signal = combined(payload.signal)
|
|
127
|
+
const info = await llm.resolveModelInfo(request.provider, request.model, signal)
|
|
128
|
+
check(signal)
|
|
129
|
+
const policy = { ...compaction.config, ...(compaction.config.modelPolicies?.find(p => p.provider === request.provider && p.model === request.model) ?? {}) }
|
|
130
|
+
const capacity = info.context?.contextWindow
|
|
131
|
+
const reserved = request.maxTokens ?? info.defaultMaxTokens ?? 0
|
|
132
|
+
if (!Number.isFinite(capacity) || capacity <= reserved + policy.headroomTokens) throw new Error('context-provenance: model switch has no message budget')
|
|
133
|
+
const threshold = Math.floor(Math.min(capacity * policy.thresholdRatio, capacity - reserved - policy.headroomTokens))
|
|
134
|
+
const retain = policy.retainTokens ?? Math.floor((capacity - reserved) * policy.retainRatio)
|
|
135
|
+
if (retain >= threshold) throw new Error('context-provenance: model switch retention exceeds budget')
|
|
136
|
+
for (let attempt = 0; ; attempt++) {
|
|
137
|
+
const measurement = meter.measure(agent.session, { ...previous, config: { ...previous.config, ...request } })
|
|
138
|
+
if (measurement.totalTokens < threshold) return request
|
|
139
|
+
if (attempt > policy.compactionRetries) throw new Error('context-provenance: model switch remains above budget')
|
|
140
|
+
const range = compactableRange(agent.session, measurement, retain, pairing)
|
|
141
|
+
if (!range) throw new Error('context-provenance: model switch has no compactable range')
|
|
142
|
+
await runtime.compactRegion(range.start, range.end, signal)
|
|
143
|
+
}
|
|
144
|
+
}, { prepend: true }))
|
|
145
|
+
return dispose
|
|
146
|
+
} catch (error) { await dispose(); throw error }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function apply(ctx, config, options = {}) {
|
|
150
|
+
const parsed = configContract.parse(config, name)
|
|
151
|
+
if (!parsed.enabled) return
|
|
152
|
+
const [{ BasicCompactionEngine: Basic }, { ToolResultPruner: Pruner }, pairing] = await Promise.all([
|
|
153
|
+
importHostCapability('@deepseek-ai/dsh-compaction-basic'), importHostCapability('@deepseek-ai/dsh-compaction-tool-result-pruner'), importHostCapability('@deepseek-ai/dsh-compaction'),
|
|
154
|
+
])
|
|
155
|
+
return mountContextRuntime(ctx, parsed, options, { Basic, Pruner, pairing })
|
|
156
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { makeSessionReference } from './retrieval-reference.mjs'
|
|
2
|
+
|
|
3
|
+
const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'
|
|
4
|
+
|
|
5
|
+
/** 官方裁剪算法不变,只在本次生成marker的Unicode位置附来源。 */
|
|
6
|
+
function referencedContent(content, headChars, reference) {
|
|
7
|
+
const result = [...content]
|
|
8
|
+
let consumed = 0
|
|
9
|
+
for (let index = 0; index < result.length; index++) {
|
|
10
|
+
const block = result[index]
|
|
11
|
+
if (block.type !== 'text') continue
|
|
12
|
+
const points = [...block.text]
|
|
13
|
+
if (consumed + points.length <= headChars) { consumed += points.length; continue }
|
|
14
|
+
const offset = headChars - consumed
|
|
15
|
+
if (points.slice(offset, offset + [...PRUNE_MARKER].length).join('') !== PRUNE_MARKER) return null
|
|
16
|
+
result[index] = { ...block, text: points.slice(0, offset).join('') + PRUNE_MARKER + `[source: ${JSON.stringify(reference)}]\n` + points.slice(offset + [...PRUNE_MARKER].length).join('') }
|
|
17
|
+
return result
|
|
18
|
+
}
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createProvenancePruner(PrunerClass, { sourceModuleId, isActive = () => true, signalOf = () => undefined, makeReference = makeSessionReference } = {}) {
|
|
23
|
+
return class ProvenancePruner extends PrunerClass {
|
|
24
|
+
pruneSession(session, signal = signalOf()) {
|
|
25
|
+
const check = () => {
|
|
26
|
+
signal?.throwIfAborted()
|
|
27
|
+
if (!isActive()) throw new Error('context-provenance: pruner released')
|
|
28
|
+
}
|
|
29
|
+
check()
|
|
30
|
+
const events = session.snapshotEvents()
|
|
31
|
+
const bySeq = new Map(events.map(event => [event.seq, event]))
|
|
32
|
+
const candidates = [...new Set(session.surface.nodes)].map(seq => bySeq.get(seq)).filter(event => event?.type === 'tool/result')
|
|
33
|
+
const pruned = []
|
|
34
|
+
let charsRemoved = 0
|
|
35
|
+
for (const event of candidates) {
|
|
36
|
+
check()
|
|
37
|
+
const seq = event.seq, original = session.deriveEventMessage(event)
|
|
38
|
+
let content = super.pruneContent(original.content)
|
|
39
|
+
if (content === null) continue
|
|
40
|
+
const charsBefore = this.measureContent(original.content)
|
|
41
|
+
try {
|
|
42
|
+
const reference = makeReference(sourceModuleId, session.id, events, [seq])
|
|
43
|
+
const cited = referencedContent(content, this.config.headChars, reference)
|
|
44
|
+
if (!cited || this.measureContent(cited) > this.config.thresholdChars || this.measureContent(cited) >= charsBefore) throw new Error('reference exceeds pruning budget')
|
|
45
|
+
content = cited
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.ctx.logger?.warn?.(`context-provenance: pruned without reference (${String(error?.message ?? error)})`)
|
|
48
|
+
}
|
|
49
|
+
const charsAfter = this.measureContent(content)
|
|
50
|
+
if (charsAfter > this.config.thresholdChars || charsAfter >= charsBefore) throw new Error('context-provenance: invalid prune result')
|
|
51
|
+
const shadowedTokenCount = this.ctx.tokenMeter.estimateMessage(original)
|
|
52
|
+
check()
|
|
53
|
+
session.append('compaction/prune', { shadowedRange: { start: seq, end: seq }, shadowedSeqs: [seq], shadowedTokenCount })
|
|
54
|
+
check()
|
|
55
|
+
const replacement = session.append('tool/result', { ...event.data, message: { ...original, content } }, {
|
|
56
|
+
surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq],
|
|
57
|
+
})
|
|
58
|
+
pruned.push({ originalSeq: seq, replacementSeq: replacement.seq, callId: original.source.callId, charsBefore, charsAfter })
|
|
59
|
+
charsRemoved += charsBefore - charsAfter
|
|
60
|
+
}
|
|
61
|
+
return { pruned, charsRemoved }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
|
|
3
|
+
/** 只路由声明Agent,原provider的方法和其它监听器保持原所有权。 */
|
|
4
|
+
export function routeContextRuntime(ctx, agent, runtime) {
|
|
5
|
+
let active = true
|
|
6
|
+
const disposers = []
|
|
7
|
+
for (const service of ['compaction', 'toolResultPruner']) {
|
|
8
|
+
if (!runtime[service]) continue
|
|
9
|
+
const dependency = ctx.inject([service], dependent => {
|
|
10
|
+
// 已发布Fiber.store公开了这次依赖快照的精确provider身份。
|
|
11
|
+
const owner = dependent.fiber.store[service].fiber
|
|
12
|
+
const carriers = new WeakMap()
|
|
13
|
+
const offDispatch = service === 'compaction' ? dependent.on('internal/dispatch', (_mode, event, args, carrier) => {
|
|
14
|
+
if (!active || !['agent/pre-step', 'agent/request-error', 'agent/request'].includes(event)
|
|
15
|
+
|| args[0]?.agent !== agent || !carrier) return
|
|
16
|
+
if (carriers.has(carrier)) return
|
|
17
|
+
const original = carrier[Context.filter]
|
|
18
|
+
const filter = function (candidate) {
|
|
19
|
+
return (!original || original.call(this, candidate)) && (!active || candidate.fiber !== owner)
|
|
20
|
+
}
|
|
21
|
+
carriers.set(carrier, original)
|
|
22
|
+
carrier[Context.filter] = filter
|
|
23
|
+
}) : () => {}
|
|
24
|
+
const offGet = dependent.on('internal/get', (_consumer, key, _error, next) => {
|
|
25
|
+
const original = next()
|
|
26
|
+
if (!active || key !== service || !original) return original
|
|
27
|
+
return new Proxy(original, {
|
|
28
|
+
get(target, property) {
|
|
29
|
+
const method = target[property]
|
|
30
|
+
if (typeof method !== 'function') return method
|
|
31
|
+
return (...args) => {
|
|
32
|
+
const subject = service === 'toolResultPruner' ? args[0] : property === 'compactRegion' ? args[2] : args[0]
|
|
33
|
+
const selected = service === 'toolResultPruner' ? subject === agent.session : subject === agent
|
|
34
|
+
if (active && selected && typeof runtime[service][property] === 'function') return runtime[service][property](...args)
|
|
35
|
+
return method.apply(target, args)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}, { prepend: true })
|
|
40
|
+
return () => { offDispatch(); offGet() }
|
|
41
|
+
})
|
|
42
|
+
disposers.push(() => dependency.dispose())
|
|
43
|
+
}
|
|
44
|
+
return () => { active = false; for (const dispose of disposers) void dispose() }
|
|
45
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { makeSessionReference } from './retrieval-reference.mjs'
|
|
2
|
+
|
|
3
|
+
// shortcut: Basic alpha.2 未公开导出frameSummary,升级官方framing时同步此版本化快照。
|
|
4
|
+
const CHECKPOINT_OPEN = 'This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n<compacted-summary>'
|
|
5
|
+
export function frameSummary(summary) {
|
|
6
|
+
return [{ type: 'text', text: CHECKPOINT_OPEN }, ...summary, { type: 'text', text: '</compacted-summary>' }]
|
|
7
|
+
}
|
|
8
|
+
const framed = summary => ({
|
|
9
|
+
id: 'context-provenance-budget', role: 'user', source: { kind: 'compact-checkpoint', compactionId: 'context-provenance-budget' },
|
|
10
|
+
content: frameSummary(summary),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function sourceSnapshot(input, session, meter) {
|
|
14
|
+
const events = session.snapshotEvents(), bySeq = new Map(events.map(event => [event.seq, event]))
|
|
15
|
+
const nodes = session.surface.nodes.map(seq => {
|
|
16
|
+
const event = bySeq.get(seq)
|
|
17
|
+
if (!event) throw new Error('source event missing')
|
|
18
|
+
return { seq, message: session.deriveEventMessage(event) }
|
|
19
|
+
})
|
|
20
|
+
let messages = input.messages
|
|
21
|
+
const head = nodes[0]?.message
|
|
22
|
+
// Basic 把system头作为缓存前缀再加一次;真正选中system时后面仍保留它。
|
|
23
|
+
if (head?.role === 'system' && messages[0]?.id === head.id) messages = messages.slice(1)
|
|
24
|
+
if (!messages.length || messages.some(message => typeof message.id !== 'string' || !message.id)) throw new Error('source message identity unavailable')
|
|
25
|
+
const matches = []
|
|
26
|
+
for (let start = 0; start < nodes.length; start++) {
|
|
27
|
+
if (nodes[start].message?.id !== messages[0].id) continue
|
|
28
|
+
let position = 0
|
|
29
|
+
for (let end = start; end < nodes.length; end++) {
|
|
30
|
+
const message = nodes[end].message
|
|
31
|
+
if (message !== null) {
|
|
32
|
+
if (message?.id !== messages[position]?.id) break
|
|
33
|
+
position++
|
|
34
|
+
}
|
|
35
|
+
if (position !== messages.length) continue
|
|
36
|
+
// 空投影可能属于区间也可能不属于;没有真实事务端点时不猜。
|
|
37
|
+
if (nodes[start - 1]?.message === null || nodes[end + 1]?.message === null) throw new Error('source span ambiguous')
|
|
38
|
+
matches.push(nodes.slice(start, end + 1).map(node => node.seq))
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (matches.length !== 1) throw new Error('source span unavailable or ambiguous')
|
|
43
|
+
const seqs = matches[0], measured = meter.measure(session).nodes
|
|
44
|
+
if (measured.length !== nodes.length || measured.some((node, index) => node.seq !== nodes[index].seq)) throw new Error('source token surface changed')
|
|
45
|
+
const selected = new Set(seqs)
|
|
46
|
+
const tokens = measured.filter(node => selected.has(node.seq)).reduce((total, node) => total + node.tokens, 0)
|
|
47
|
+
if (!Number.isFinite(tokens) || tokens <= 0) throw new Error('source token budget unavailable')
|
|
48
|
+
return { events, seqs, tokens }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createProvenanceCompaction(BasicClass, { sourceModuleId, isActive = () => true, signalOf = () => undefined, makeReference = makeSessionReference } = {}) {
|
|
52
|
+
return class ProvenanceCompaction extends BasicClass {
|
|
53
|
+
async summarize(input, agent, signal = signalOf(agent)) {
|
|
54
|
+
const check = () => {
|
|
55
|
+
signal?.throwIfAborted()
|
|
56
|
+
if (!isActive(agent)) throw new DOMException('context-provenance: compaction released', 'AbortError')
|
|
57
|
+
}
|
|
58
|
+
const warn = message => { try { this.ctx.logger?.warn?.('context-provenance: summary without reference (' + message + ')') } catch { /* 诊断不能使原本可用的压缩失败。 */ } }
|
|
59
|
+
check()
|
|
60
|
+
let snapshot
|
|
61
|
+
try { snapshot = sourceSnapshot(input, agent.session, this.ctx.tokenMeter) } catch (error) { warn(String(error?.message ?? error)) }
|
|
62
|
+
const result = await super.summarize(input, agent, signal)
|
|
63
|
+
check()
|
|
64
|
+
if (!snapshot) return result
|
|
65
|
+
try {
|
|
66
|
+
const reference = makeReference(sourceModuleId, agent.session.id, snapshot.events, snapshot.seqs)
|
|
67
|
+
const summary = [{ type: 'text', text: `[source: ${JSON.stringify(reference)}]\n` }, ...result.summary]
|
|
68
|
+
const tokens = this.ctx.tokenMeter.estimateMessage(framed(summary))
|
|
69
|
+
if (!Number.isFinite(tokens) || tokens >= snapshot.tokens) {
|
|
70
|
+
warn('reference exceeds shrink budget')
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
return { ...result, summary }
|
|
74
|
+
} catch (error) {
|
|
75
|
+
warn(String(error?.message ?? error))
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|