@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/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 拒绝态)→ 阈值保护(D6)→ ctx.compact() fire-and-forget
5
- * (R2 实测 2026-08-22:tool execute 内 await ctx.compact() 不可行——AgentSession.compact
6
- * 开头的 abort() 会中止当前 agent 循环,挂起的 Promise 永不兑现、session 无 toolResult。
7
- * 故走 §3.2 降级态:execute 立即返回"压缩已启动",onComplete/onError 后经
8
- * sendSmartContextNotice 注入结果消息(nextTurn 投递路径,D4①——随下一次 prompt 注入,
9
- * 不自起 run;此刻无进行中回合,compact 内部 abort 为 no-op)。
10
- * 压缩生成由 session_before_compact 接管 handler 完成(工具只触发,不生成)。
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
- * 掐断的 turn 由 runtime 兜底续跑(设计 D4②):本工具触发的 manual compact 会 abort 进行中
13
- * 的 agent turn(pi F5:manual compact 明文不续跑),runtime 在 compaction_end 判定
14
- * 「manual + 非 runtime 发起 + compaction_start 前 turn 被掐断」后经投递内核追加续跑投递。
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
- /** CompactionResult 的宽松形状(onComplete 回调参数消费字段)。 */
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
- * 收起态一行摘要。R2 降级态下工具返回时压缩尚未完成(结果异步注入),无保留比例
73
- * 数据可取——恒为 "Context compacted";完成后的比例数据走注入消息,不进本摘要。
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 "Context compacted";
81
+ return "后台压缩已启动";
77
82
  }
78
83
 
79
- /** 工具描述(§3.5:三条件自查引导;条件即纪律,无第三层复述——D1 单源化核对通过)。 */
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
- * G1 代际检测(crash-resilience D1):compact 是 fire-and-forget——onComplete/onError
126
- * 回调在压缩 LLM 调用期间异步触发,用户此间切换/重载 session 后回调持有的 pi 已
127
- * stale(E1 实锤崩溃点)。index.ts 注入模块级代数比对闭包,前置检查 + 错误分诊
128
- * 双保险;缺省时检查退化为纯文案兜底分诊(PS-30 门禁守文案)。
108
+ * 后台压缩任务依赖(BackgroundCompactionControls 契约)。未注入 = 生产接线
109
+ * 缺失:execute 在门控与阈值检查后抛「后台压缩未接线」错误(fail-fast)。
129
110
  */
130
- isCtxStale?: () => boolean;
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
- const compactionCount = countCompactions(getEntries(ctx));
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
- // R2 降级态:fire-and-forget。工具立即返回"已启动";压缩完成后(此时无进行中
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
- ctx.compact({
183
- customInstructions:
184
- typeof params.custom_instructions === "string" && params.custom_instructions.trim() !== ""
185
- ? params.custom_instructions
186
- : undefined,
187
- // E1 实锤崩溃点(9/3 pi-crash log):两个回调由 compact 的内部 Promise 链异步
188
- // 调用,不在 pi runner emit() 的 try/catch 内——session 替换窗口(GUI 切
189
- // session/新建/重载高频触发)下 sendMessage 命中 stale ctx 同步抛错即
190
- // 杀死 pi 进程(pi 1.0.0 实装:dist/core/extensions/loader.js 的 assertActive
191
- // wrapper 对 stale 即 throw "no longer active",在 runtime 调用前同步 throw)。
192
- // 检查 stale 静默降级(结果不投递,用户可重试 /compact),非
193
- // stale 错误原样上抛(检查不吞真实 bug)。
194
- onComplete: (r: unknown) => {
195
- guardStaleCtx(() => {
196
- const result = toCompactionResultLike(r);
197
- const resultMode = result.details?.mode ?? "native-fallback";
198
- const fellBack = resultMode === "native-fallback";
199
- if (fellBack) debugLog("compact_context: takeover fell back to native generation");
200
- const cacheRead = result.usage?.cacheRead;
201
- const cost = result.usage
202
- ? `${formatK(result.usage.input ?? 0)} input${cacheRead ? `(其中缓存命中 ${formatK(cacheRead)})` : ""} + ${formatK(result.usage.output ?? 0)} output`
203
- : "未知";
204
- const showHint = compactionCount + 1 >= DEGRADATION_HINT_MIN_COMPACTIONS;
205
- const lines = [
206
- `[smart-context] 压缩完成。模式:${resultMode}${fellBack ? "(压缩模型不可用,已回退当前模型——请检查配置:taiji 设置页或 smart-context-ext-config skill)" : ""}。`,
207
- `压缩前 ${formatK(result.tokensBefore ?? 0)} tokens → 压缩后约 ${formatK(result.estimatedTokensAfter ?? 0)} tokens;摘要生成成本:${cost}。`,
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
  };