@zhushanwen/pi-smart-context 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +6 -5
- package/skills/smart-context-ext-config/SKILL.md +6 -2
- package/src/__tests__/compact-handler.test.ts +1178 -6
- package/src/__tests__/index-compaction-signal.test.ts +207 -0
- package/src/__tests__/index-generation.test.ts +34 -39
- package/src/__tests__/index-manual-compact-command.test.ts +262 -0
- package/src/__tests__/index-reminder-delivery.test.ts +1 -1
- package/src/__tests__/notices.test.ts +194 -0
- package/src/__tests__/pure.test.ts +20 -2
- package/src/__tests__/summary.test.ts +11 -8
- package/src/__tests__/tool.test.ts +88 -79
- package/src/compact-handler.ts +947 -28
- package/src/index.ts +236 -17
- package/src/notices.ts +189 -3
- package/src/pure.ts +17 -1
- package/src/reminder.ts +4 -3
- package/src/tool.ts +85 -125
package/src/tool.ts
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* compact_context 工具(D6 + §3.5
|
|
2
|
+
* compact_context 工具(D6 + §3.5 接口契约;U4 纯触发化——§3.1 入口矩阵①)。
|
|
3
3
|
*
|
|
4
|
-
* execute 流程:门控校验(D5
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* execute 流程:门控校验(D5 拒绝态,V7 现状文案)→ 阈值保护(D6 低 tokens 拒绝)→
|
|
5
|
+
* 后台任务触发(经 deps.background 注入的共享后台任务管理器能力)→ 立即返回成功回执
|
|
6
|
+
* (GL1:不等待摘要生成;压缩执行期对话零阻塞零掐断——不触碰 ctx.compact /
|
|
7
|
+
* AgentSession 压缩路径,旧路径掐断进行中回合的 abort 副作用随之消失)。
|
|
8
|
+
* 已有后台任务进行中时返回「后台压缩进行中」的幂等成功回执(不是错误;RL1 单态互斥)。
|
|
9
|
+
* 压缩生成→校验→落盘由后台任务自管(compact-handler.ts 后台任务管理器);
|
|
10
|
+
* 完成/失败通知经 nextTurn 注入(U5 接线)。
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* deps.background 是生产接线契约(BackgroundCompactionControls):注册点注入与
|
|
13
|
+
* agent_settled 自动触发共享同一 session 级互斥容器的查询/启动能力(RL1);
|
|
14
|
+
* 未注入 = 接线缺失,execute 在门控与阈值检查后 fail-fast(不撒谎回执、不静默降级)。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { guardStaleCtx, isRecord, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
19
18
|
import { Type } from "typebox";
|
|
20
19
|
|
|
21
|
-
import { debugLog } from "./compact-handler.js";
|
|
22
|
-
import { sendSmartContextNotice } from "./notices.js";
|
|
23
20
|
import { renderCompactContextResult } from "./render-result.js";
|
|
24
|
-
import { buildDegradationHintLine } from "./reminder.js";
|
|
25
21
|
import {
|
|
26
|
-
DEGRADATION_HINT_MIN_COMPACTIONS,
|
|
27
22
|
checkToolThresholdGuard,
|
|
28
23
|
countCompactions,
|
|
29
|
-
formatK,
|
|
30
24
|
getCurrentModelId,
|
|
31
25
|
isGatingActive,
|
|
32
26
|
loadSmartContextConfig,
|
|
@@ -47,73 +41,62 @@ const CompactContextParams = Type.Object(
|
|
|
47
41
|
{ additionalProperties: false },
|
|
48
42
|
);
|
|
49
43
|
|
|
50
|
-
/**
|
|
51
|
-
interface CompactionResultLike {
|
|
52
|
-
tokensBefore?: number;
|
|
53
|
-
estimatedTokensAfter?: number;
|
|
54
|
-
usage?: { input?: number; output?: number; cacheRead?: number };
|
|
55
|
-
details?: { engine?: string; mode?: string } & Record<string, unknown>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 工具 details(renderResult 数据源,规范:不依赖 content 文本解析)。R2 降级态:启动即返回。 */
|
|
44
|
+
/** 工具 details(renderResult 数据源,规范:不依赖 content 文本解析)。 */
|
|
59
45
|
export interface CompactContextDetails {
|
|
60
46
|
/** 启动时的模式判定(cross-model 时为配置的压缩模型 ref)。 */
|
|
61
47
|
mode: string;
|
|
62
48
|
compactModel: string;
|
|
63
49
|
compactionCount: number;
|
|
64
|
-
/** 压缩已触发(结果经注入消息送达,不在工具结果里)。 */
|
|
65
|
-
launched: boolean;
|
|
66
|
-
fellBack: boolean;
|
|
67
50
|
/** 收起态一行摘要(buildCompactContextSummary 产出;GUI 收起态与 TUI 折叠态共用)。 */
|
|
68
51
|
summary?: string;
|
|
69
52
|
}
|
|
70
53
|
|
|
71
54
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
55
|
+
* 后台压缩任务依赖(U4 工具路径接线契约;U5 起由 index.ts 注册点注入生产实装)。
|
|
56
|
+
*
|
|
57
|
+
* 注入的实现必须与 agent_settled 自动触发共享同一 session 级互斥容器
|
|
58
|
+
* (RL1:同一时刻至多一个后台压缩任务在执行),即 compact-handler.ts
|
|
59
|
+
* createBackgroundCompactionManager 所用 BackgroundCompactionState 的能力面。
|
|
60
|
+
*/
|
|
61
|
+
export interface BackgroundCompactionControls {
|
|
62
|
+
/** 互斥查询:当前 session 是否有进行中的后台压缩任务(空闲⇄进行中单态判定)。 */
|
|
63
|
+
isActive: () => boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 启动后台压缩任务(工具路径 = 显式请求:不做触发线(D4 ratio×窗口)与独立熔断
|
|
66
|
+
* (SI5)判定——两者仅约束 agent_settled 自动触发入口)。
|
|
67
|
+
* 返回 true = 已启动;false = 已有任务进行中(isActive 与本调用之间竞态窗口的
|
|
68
|
+
* 兜底判定,调用方按互斥幂等处理)——false 是唯一的互斥语义,非互斥原因不可启动
|
|
69
|
+
* (门控拒绝 / 压缩接口探测失败 / 无可压内容)由实现抛错上浮(诚实失败,不谎报幂等)。
|
|
70
|
+
* 生成结果不经本调用返回(触发即返回):完成/失败经通知通道送达(U5)。
|
|
71
|
+
*/
|
|
72
|
+
start: (ctx: ExtensionContext, customInstructions?: string) => boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 收起态一行摘要(GUI 收起态与 TUI 折叠态共用,经 details.summary 渲染)。
|
|
77
|
+
* 压缩在后台执行:工具返回时任务刚启动,收起态/历史态语义 = 「后台压缩已启动」;
|
|
78
|
+
* 「已完成」语义由完成通知承载(U5/D6),比例数据走完成通知不进本摘要。
|
|
74
79
|
*/
|
|
75
80
|
export function buildCompactContextSummary(): string {
|
|
76
|
-
return "
|
|
81
|
+
return "后台压缩已启动";
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
/** 工具描述(§3.5
|
|
84
|
+
/** 工具描述(§3.5:三条件自查引导;条件即纪律,无第三层复述)。压缩在后台执行(U4 纯触发化)。 */
|
|
80
85
|
const TOOL_DESCRIPTION =
|
|
81
86
|
"压缩当前会话上下文(释放早期对话占用的 token;压缩由配置的模型执行,不会切换你的主模型)。" +
|
|
87
|
+
"压缩在后台进行:调用后立即返回,生成期间你可以继续任何工作,完成后的结果消息会送达会话。" +
|
|
82
88
|
"仅在同时满足以下条件时调用:1) 当前任务的一个阶段已完成并验证(如一批文件改完、测试通过);" +
|
|
83
89
|
"2) 后续工作不再依赖将被压缩的早期细节;3) 上下文已超过提醒阈值(你会收到 [smart-context 提示])。";
|
|
84
90
|
|
|
85
|
-
/** CompactionResult → 宽松形状的 guard 转换(onComplete 回调参数消费,避免 cast)。 */
|
|
86
|
-
function toCompactionResultLike(result: unknown): CompactionResultLike {
|
|
87
|
-
const r: Record<string, unknown> = isRecord(result) ? result : {};
|
|
88
|
-
const usage = isRecord(r.usage) ? r.usage : null;
|
|
89
|
-
const details = isRecord(r.details) ? r.details : null;
|
|
90
|
-
const num = (v: unknown) => (typeof v === "number" ? v : undefined);
|
|
91
|
-
const str = (v: unknown) => (typeof v === "string" ? v : undefined);
|
|
92
|
-
return {
|
|
93
|
-
tokensBefore: num(r.tokensBefore),
|
|
94
|
-
estimatedTokensAfter: num(r.estimatedTokensAfter),
|
|
95
|
-
usage: usage
|
|
96
|
-
? {
|
|
97
|
-
input: num(usage.input),
|
|
98
|
-
output: num(usage.output),
|
|
99
|
-
cacheRead: num(usage.cacheRead),
|
|
100
|
-
}
|
|
101
|
-
: undefined,
|
|
102
|
-
details: details
|
|
103
|
-
? {
|
|
104
|
-
engine: str(details.engine),
|
|
105
|
-
mode: str(details.mode),
|
|
106
|
-
}
|
|
107
|
-
: undefined,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
91
|
/**
|
|
112
92
|
* 注册 compact_context 工具。
|
|
113
93
|
*
|
|
114
|
-
* gatingProbe/usageProbe 为依赖注入(单测 mock 点):
|
|
94
|
+
* gatingProbe/usageProbe/getEntries/background 为依赖注入(单测 mock 点):
|
|
115
95
|
* - gatingProbe(ctx) → { active, modelId }(生产实现:现场读配置判定)
|
|
116
96
|
* - usageProbe(ctx) → { tokens, contextWindow }(生产实现:ctx.getContextUsage())
|
|
97
|
+
* - getEntries(ctx) → 条目列表(生产实现:ctx.sessionManager.getEntries())
|
|
98
|
+
* - background:后台任务互斥查询与启动(BackgroundCompactionControls 契约;
|
|
99
|
+
* U5 起 index.ts 注册点注入生产实装,与自动触发共享互斥容器)
|
|
117
100
|
*/
|
|
118
101
|
export function registerCompactContextTool(
|
|
119
102
|
pi: ExtensionAPI,
|
|
@@ -122,12 +105,10 @@ export function registerCompactContextTool(
|
|
|
122
105
|
usageProbe?: (ctx: ExtensionContext) => { tokens: number | null; contextWindow: number };
|
|
123
106
|
getEntries?: (ctx: ExtensionContext) => ReadonlyArray<EntryLike>;
|
|
124
107
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* stale(E1 实锤崩溃点)。index.ts 注入模块级代数比对闭包,前置检查 + 错误分诊
|
|
128
|
-
* 双保险;缺省时检查退化为纯文案兜底分诊(PS-30 门禁守文案)。
|
|
108
|
+
* 后台压缩任务依赖(BackgroundCompactionControls 契约)。未注入 = 生产接线
|
|
109
|
+
* 缺失:execute 在门控与阈值检查后抛「后台压缩未接线」错误(fail-fast)。
|
|
129
110
|
*/
|
|
130
|
-
|
|
111
|
+
background?: BackgroundCompactionControls;
|
|
131
112
|
},
|
|
132
113
|
): void {
|
|
133
114
|
const probeGating =
|
|
@@ -156,7 +137,7 @@ export function registerCompactContextTool(
|
|
|
156
137
|
description: TOOL_DESCRIPTION,
|
|
157
138
|
parameters: CompactContextParams,
|
|
158
139
|
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
159
|
-
// D5
|
|
140
|
+
// D5 门控现场校验(配置热改即时生效,不依赖注册时机;V7:拒绝回现状文案)
|
|
160
141
|
const gating = probeGating(ctx);
|
|
161
142
|
const config = loadSmartContextConfig();
|
|
162
143
|
if (!gating.active) {
|
|
@@ -166,84 +147,63 @@ export function registerCompactContextTool(
|
|
|
166
147
|
throw new Error(reason);
|
|
167
148
|
}
|
|
168
149
|
|
|
169
|
-
// D6 阈值保护(含 null
|
|
150
|
+
// D6 阈值保护(含 null 分支;A9:现状文案不动)
|
|
170
151
|
const usage = probeUsage(ctx);
|
|
171
152
|
const guardMessage = checkToolThresholdGuard(config.reminderThresholds, usage.tokens);
|
|
172
153
|
if (guardMessage !== null) {
|
|
173
154
|
throw new Error(guardMessage);
|
|
174
155
|
}
|
|
175
156
|
|
|
176
|
-
|
|
157
|
+
// 后台任务依赖(接线契约):未注入 = 生产接线缺失 → fail-fast。接线上线前
|
|
158
|
+
// 的窗口期工具显式不可用,不撒谎回执、不静默降级
|
|
159
|
+
const background = deps?.background;
|
|
160
|
+
if (!background) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"compact_context 后台压缩未接线:工具注册点缺少 background 依赖注入(BackgroundCompactionControls)。" +
|
|
163
|
+
"请在 smart-context 扩展注册处注入与自动触发共享互斥容器的查询/启动接口。",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
177
166
|
|
|
178
|
-
|
|
179
|
-
// 回合,compact 内部的 abort 为 no-op)经 sendSmartContextNotice 注入结果
|
|
180
|
-
// (nextTurn:随下一次 prompt 作为 custom role 上下文注入,不自起 run——D4①)
|
|
167
|
+
const compactionCount = countCompactions(getEntries(ctx));
|
|
181
168
|
const mode = pickMode(config, gating.modelId);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
showHint ? buildDegradationHintLine() : "",
|
|
209
|
-
].filter((l) => l !== "");
|
|
210
|
-
sendSmartContextNotice(pi, lines.join("\n"), "compact-complete");
|
|
211
|
-
}, {
|
|
212
|
-
isCtxStale: deps?.isCtxStale,
|
|
213
|
-
label: "smart-context:compact-onComplete",
|
|
214
|
-
onStale: (error) => debugLog(`compact result delivery skipped (stale ctx): ${toErrorMessage(error)}`),
|
|
215
|
-
});
|
|
216
|
-
},
|
|
217
|
-
onError: (err: Error) => {
|
|
218
|
-
// 压缩本身的失败信息先落日志(检查体外——stale 降级时该观测保留)
|
|
219
|
-
debugLog(`compact_context error: ${err.message}`);
|
|
220
|
-
guardStaleCtx(() => {
|
|
221
|
-
sendSmartContextNotice(
|
|
222
|
-
pi,
|
|
223
|
-
`[smart-context] 压缩失败:${err.message}。上下文未变化,可稍后重试(若反复失败,检查 smart-context 配置或使用 /compact)。`,
|
|
224
|
-
"compact-failed",
|
|
225
|
-
);
|
|
226
|
-
}, {
|
|
227
|
-
isCtxStale: deps?.isCtxStale,
|
|
228
|
-
label: "smart-context:compact-onError",
|
|
229
|
-
onStale: (error) => debugLog(`compact failure notice delivery skipped (stale ctx): ${toErrorMessage(error)}`),
|
|
230
|
-
});
|
|
231
|
-
},
|
|
232
|
-
});
|
|
169
|
+
const customInstructions =
|
|
170
|
+
typeof params.custom_instructions === "string" && params.custom_instructions.trim() !== ""
|
|
171
|
+
? params.custom_instructions
|
|
172
|
+
: undefined;
|
|
173
|
+
|
|
174
|
+
// U4 纯触发化:启动后台任务立即回执(GL1 不等待摘要生成)。互斥幂等(RL1):
|
|
175
|
+
// 已进行中不再启动,返回成功回执而非错误——isActive 与 start 之间的竞态由
|
|
176
|
+
// start 返回 false 兜底,同样按幂等处理
|
|
177
|
+
const started = !background.isActive() && background.start(ctx, customInstructions);
|
|
178
|
+
if (!started) {
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: "后台压缩进行中,本次请求已并入该任务,无需重复触发。压缩在后台进行,你可以继续任何工作;完成后会收到一条结果消息。",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
details: {
|
|
187
|
+
mode,
|
|
188
|
+
compactModel: mode === "cross-model" ? config.compactModel.ref : gating.modelId,
|
|
189
|
+
// 无新任务启动,压缩序号不变
|
|
190
|
+
compactionCount,
|
|
191
|
+
summary: buildCompactContextSummary(),
|
|
192
|
+
} satisfies CompactContextDetails,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
233
195
|
|
|
234
196
|
return {
|
|
235
197
|
content: [
|
|
236
198
|
{
|
|
237
199
|
type: "text",
|
|
238
|
-
text: `压缩已启动(${mode === "same-model" ? "same-model 模式,KV 缓存优化" : `cross-model 模式,使用 ${config.compactModel.ref}`}
|
|
200
|
+
text: `压缩已启动(${mode === "same-model" ? "same-model 模式,KV 缓存优化" : `cross-model 模式,使用 ${config.compactModel.ref}`})。压缩在后台进行,你可以继续任何工作;完成后会收到一条结果消息。`,
|
|
239
201
|
},
|
|
240
202
|
],
|
|
241
203
|
details: {
|
|
242
204
|
mode,
|
|
243
205
|
compactModel: mode === "cross-model" ? config.compactModel.ref : gating.modelId,
|
|
244
|
-
fellBack: false,
|
|
245
206
|
compactionCount: compactionCount + 1,
|
|
246
|
-
launched: true,
|
|
247
207
|
summary: buildCompactContextSummary(),
|
|
248
208
|
} satisfies CompactContextDetails,
|
|
249
209
|
};
|