@zhushanwen/pi-smart-context 0.4.1 → 0.4.3

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 CHANGED
@@ -10,7 +10,7 @@
10
10
  - cross-model:调用 pi 原生 `compact()` 仅换模型与凭证——split-turn / fileOps / previousSummary 原生组装
11
11
  - **3 档阈值提醒**(默认 400K/500K/600K):`agent_settled` 越档检查,每档一次、多档合并、压缩后重置,nextTurn 投递(随下一次 prompt 注入,不自起 run);已提醒档位经 `pi.appendEntry` 落在 session entries(`smart-context:fired`),reload / 进程重启后从 entries 重建,同一档不会重复提醒;措辞是数据投递不是指令
12
12
  - **排除模型**(精准 `provider/modelId` 匹配):工具拒绝 + 不提醒 + 回落 pi 原生生成;切换跨界时注入一条可用性通知,downshift(切小窗模型将触线)时建议先压缩
13
- - **通知车道**:压缩结果 / 阈值提醒 / 模型切换四类通知统一走 nextTurn(custom role 随下次 prompt 注入,不自起 run)——工具压缩掐断的 turn 由 taiji runtime 在 `compaction_end` 判定后经投递内核追加续跑投递
13
+ - **通知投递路径**:压缩结果 / 阈值提醒 / 模型切换四类通知统一走 nextTurn(custom role 随下次 prompt 注入,不自起 run)——工具压缩掐断的 turn 由 taiji runtime 在 `compaction_end` 判定后经投递内核追加续跑投递
14
14
  - **健壮性**:摘要收缩校验、max-tokens 截断 fail-closed、接管失败 3 次熔断(本 session 内停止接管)、transcript 回查指针、压缩后最近文件内容重注入(≤5 文件/50K)、多轮压缩降智提示(累计压缩 ≥2 次后附加)
15
15
  - **subagent 进程**自动静默(`TAIJI_AGENT_SUBAGENT` 标记)
16
16
 
@@ -32,15 +32,9 @@
32
32
  pi install npm:@zhushanwen/pi-smart-context
33
33
  ```
34
34
 
35
- ## 工具激活(deferred)
35
+ ## 工具激活
36
36
 
37
- `compact_context` 工具为 `exposure: "deferred"`——不占常驻工具声明,由 pi 内置 `tool_search` 工具检索命中后按需激活。taiji 宿主内开箱即用(宿主启动时已激活 `tool_search`);独立 pi 使用需在 `<agentDir>/settings.json`(默认 `~/.pi/agent/settings.json`)开启:
38
-
39
- ```json
40
- { "defaultTools": ["+tool_search"] }
41
- ```
42
-
43
- `+` 前缀是增量修饰(保留 read/bash/edit/write 默认集)。未开启 `tool_search` 时该工具对模型不可见。
37
+ `compact_context` 工具为 direct 常驻——注册不声明 `exposure` 字段(pi SDK 缺省 `direct`),工具声明随会话常驻、对模型默认可见,无需经 `tool_search` 检索激活,独立 pi 开箱即用。
44
38
 
45
39
  ## 配置
46
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-smart-context",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "taiji": {
@@ -36,7 +36,7 @@
36
36
  "pi-package"
37
37
  ],
38
38
  "dependencies": {
39
- "@zhushanwen/extension-protocol": "0.23.0",
39
+ "@zhushanwen/extension-protocol": "0.25.0",
40
40
  "@zhushanwen/pi-ext-guards": "0.5.0",
41
41
  "@zhushanwen/pi-extension-logger": "0.6.2",
42
42
  "@zhushanwen/pi-llm-shared": "0.12.0"
@@ -72,7 +72,7 @@
72
72
  }
73
73
  },
74
74
  "scripts": {
75
- "test": "vitest run",
75
+ "test": "vitest run --pool=threads",
76
76
  "test:watch": "vitest"
77
77
  }
78
78
  }
@@ -12,11 +12,14 @@ vi.mock("../pure.js", async (importOriginal) => {
12
12
  import { DEFAULT_SMART_CONTEXT_CONFIG, loadSmartContextConfig } from "../pure.js";
13
13
  import { registerCompactContextTool } from "../tool.js";
14
14
  import { countCompactions } from "../pure.js";
15
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
16
16
 
17
17
  interface RegisteredTool {
18
18
  name: string;
19
+ /** pi SDK 的 exposure 缺省值 "direct"(常驻)——不写字段时 SDK 侧默认(pi-semantics PS-91),本视图可缺省。 */
20
+ exposure?: string;
19
21
  parameters: { type?: string };
22
+ renderResult?: (result: unknown, options: { expanded: boolean }, theme: Theme) => { render: (width: number) => string[] };
20
23
  execute: (id: string, params: Record<string, unknown>, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ content: Array<{ type: string; text: string }>; details: Record<string, unknown> }>;
21
24
  }
22
25
 
@@ -67,6 +70,17 @@ describe("compact_context 工具(R2 降级态:fire-and-forget + 结果注入
67
70
  expect(tools[0].name).toBe("compact_context");
68
71
  });
69
72
 
73
+ // exposure 重划终态锚(C-ext-05,2026-10-23 ADR-0136):direct 常驻 = 注册时不写
74
+ // exposure 字段(SDK 缺省 direct,pi-semantics PS-91)——误写 "deferred" 会把压缩入口
75
+ // 从模型默认可见集收走(tool_search 才可激活),此处红。
76
+ it("exposure 注册终态:direct 常驻——不写 exposure 字段(SDK 缺省 direct)", () => {
77
+ const { pi, tools } = makePi();
78
+ registerCompactContextTool(pi);
79
+ expect(tools).toHaveLength(1);
80
+ expect(tools[0]!.name).toBe("compact_context");
81
+ expect(tools[0]!.exposure).toBeUndefined();
82
+ });
83
+
70
84
  it("门控拒绝(排除命中)→ throw 带原因与恢复指引(D5)", async () => {
71
85
  mockedLoad.mockReturnValue({ ...DEFAULT_SMART_CONTEXT_CONFIG, excludedModels: ["zai/glm"] });
72
86
  const { pi, tools } = makePi();
@@ -95,13 +109,26 @@ describe("compact_context 工具(R2 降级态:fire-and-forget + 结果注入
95
109
  expect(result.details).toMatchObject({ mode: "same-model", launched: true, fellBack: false, compactionCount: 1 });
96
110
  });
97
111
 
112
+ it("renderResult 接线(C-ext-05):注册定义携带函数成员 + 折叠态输出 details.summary 一行", async () => {
113
+ const { pi, tools } = makePi();
114
+ registerCompactContextTool(pi, { getEntries: () => [{ type: "message" }] });
115
+ const tool = tools[0];
116
+ expect(typeof tool.renderResult).toBe("function");
117
+ const result = await tool.execute("t1", {}, undefined, undefined, makeCtx());
118
+ const renderTheme = { fg: (_token: string, text: string) => text } as unknown as Theme;
119
+ const lines = tool.renderResult!(result, { expanded: false }, renderTheme).render(400);
120
+ // Text.render 按宽度 pad,trim 后断言(折叠态 = details.summary 一行)
121
+ expect(lines.length).toBe(1);
122
+ expect(lines[0].trim()).toBe("Context compacted");
123
+ });
124
+
98
125
  it("onComplete 兑现后 nextTurn 注入结果(含模式/前后 tokens/成本;降智提示按次数)", async () => {
99
126
  const { pi, tools } = makePi();
100
127
  registerCompactContextTool(pi, { getEntries: () => [{ type: "compaction" }, { type: "compaction" }] });
101
128
  await tools[0].execute("t1", {}, undefined, undefined, makeCtx());
102
129
  expect(pi.sendMessage).toHaveBeenCalledTimes(1);
103
130
  const [message, options] = pi.sendMessage.mock.calls[0];
104
- // D4①:nextTurn 化——custom role 消息 + 不自起 run 的投递车道
131
+ // D4①:nextTurn 化——custom role 消息 + 不自起 run 的投递路径
105
132
  expect(message.customType).toBe("smart-context");
106
133
  expect(message.details).toEqual({ source: "compact-complete" });
107
134
  expect(options).toEqual({ triggerTurn: false, deliverAs: "nextTurn" });
@@ -111,7 +138,7 @@ describe("compact_context 工具(R2 降级态:fire-and-forget + 结果注入
111
138
  expect(text).toContain("500K");
112
139
  expect(text).toContain("24K");
113
140
  expect(text).toContain("compacted multiple times");
114
- // 旧车道(自起 run 的 user 消息)已退役:不再调用 sendUserMessage(mock 挂 spy,被调用即断言红)
141
+ // 旧投递路径(自起 run 的 user 消息)已退役:不再调用 sendUserMessage(mock 挂 spy,被调用即断言红)
115
142
  expect(pi.sendUserMessage).not.toHaveBeenCalled();
116
143
  });
117
144
 
@@ -124,7 +151,7 @@ describe("compact_context 工具(R2 降级态:fire-and-forget + 结果注入
124
151
  expect(pi.sendMessage.mock.calls[0][0].content).toContain("回退");
125
152
  });
126
153
 
127
- it("onError → 注入失败消息带重试指引(display:true 失败必须用户可见 + nextTurn 车道不自起 run)", async () => {
154
+ it("onError → 注入失败消息带重试指引(display:true 失败必须用户可见 + nextTurn 投递路径不自起 run)", async () => {
128
155
  const { pi, tools } = makePi();
129
156
  registerCompactContextTool(pi);
130
157
  const ctx = makeCtx((options) => options.onError(new Error("Nothing to compact")));
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * - agent_settled:越档检查 + marker 持久化(D15)+ nextTurn 投递一次性提醒(D3/D4①)
10
10
  * - model_select:跨界通知 + downshift 提醒(D5/D4①)
11
11
  *
12
- * 通知注入车道(D4①,delivery-ownership-kernel):四类通知全部经 notices.ts 的
12
+ * 通知投递路径(D4①,delivery-ownership-kernel):四类通知全部经 notices.ts 的
13
13
  * sendSmartContextNotice 走 nextTurn(随下一次 prompt 注入、不自起 run)——通知 run 不再
14
14
  * 抢占用户消息投递跑道;被压缩掐断的 turn 由 runtime 续跑投递兜底(D4②)。
15
15
  */
package/src/notices.ts CHANGED
@@ -33,13 +33,13 @@ export type SmartContextNoticeSource =
33
33
  | "model-downshift";
34
34
 
35
35
  /**
36
- * 注入一条 smart-context 通知(nextTurn 车道,不自起 run)。
36
+ * 注入一条 smart-context 通知(nextTurn 投递路径,不自起 run)。
37
37
  *
38
- * `deliverAs:'nextTurn'` 是投递车道(pi 实装分支序:nextTurn 分支先命中,`triggerTurn`
38
+ * `deliverAs:'nextTurn'` 是投递路径之一(pi 实装分支序:nextTurn 分支先命中,`triggerTurn`
39
39
  * 不再参与判定);`triggerTurn:false` 是意图的显式声明(双写防未来实现分支序漂移)。
40
- * `display:true` 使通知在对话流可见——分车道语义(D4①):compact-result 两条(complete/
40
+ * `display:true` 使通知在对话流可见——可见性按投递路径区分(D4①):compact-result 两条(complete/
41
41
  * failed)改造前即 display:true 系统消息,可见性保持;阈值提醒/模型跨界/downshift 三条
42
- * 改造前 display:false 静默(只进 LLM 上下文,不进对话流),随 nextTurn 车道统一转
42
+ * 改造前 display:false 静默(只进 LLM 上下文,不进对话流),随 nextTurn 投递路径统一转
43
43
  * display:true 可见。
44
44
  */
45
45
  export function sendSmartContextNotice(
package/src/reminder.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * 提醒与通知文案构造(D3/D4/D5/D13-12)。
3
3
  *
4
4
  * 全部为纯函数:文案 + 越档判定。注入由 src/index.ts / src/tool.ts 经 notices.ts 的
5
- * sendSmartContextNotice(nextTurn 车道,D4①)执行。
5
+ * sendSmartContextNotice(nextTurn 投递路径,D4①)执行。
6
6
  * 措辞原则(目标 3):提醒是数据投递不是指令——给三条件自查清单 + 明确的"可忽略"出口,
7
7
  * 避免 agent 见提醒就压缩。
8
8
  */
@@ -4,32 +4,7 @@
4
4
 
5
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
- import { firstContentText } from "@zhushanwen/extension-protocol";
8
-
9
- /** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
10
- function contentText(result: unknown): string {
11
- const content = fieldValue(result, "content");
12
- if (!Array.isArray(content)) return "";
13
- return firstContentText({ content });
14
- }
15
-
16
- /** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
17
- function fieldValue(v: unknown, key: string): unknown {
18
- if (typeof v !== "object" || v === null) return undefined;
19
- return (v as Record<string, unknown>)[key];
20
- }
21
-
22
- /** 从 details 安全读取 summary(容错非对象形态与旧版本 entry)。 */
23
- function readDetailsSummary(result: unknown): string | undefined {
24
- const summary = fieldValue(fieldValue(result, "details"), "summary");
25
- return typeof summary === "string" && summary !== "" ? summary : undefined;
26
- }
27
-
28
- /** 折叠态回退:content 文本首行(保持一行摘要形态)。 */
29
- function firstLine(text: string): string {
30
- const idx = text.indexOf("\n");
31
- return idx === -1 ? text : text.slice(0, idx);
32
- }
7
+ import { firstLine, readDetailsSummary, resultContentText } from "@zhushanwen/extension-protocol";
33
8
 
34
9
  /** 折叠态(expanded=false)= details.summary 一行(dim);展开态 = content 纯文本。 */
35
10
  export function renderCompactContextResult(
@@ -37,7 +12,7 @@ export function renderCompactContextResult(
37
12
  options: { expanded: boolean },
38
13
  theme: Theme,
39
14
  ): Text {
40
- if (options.expanded) return new Text(contentText(result), 0, 0);
41
- const summary = readDetailsSummary(result) ?? firstLine(contentText(result));
15
+ if (options.expanded) return new Text(resultContentText(result), 0, 0);
16
+ const summary = readDetailsSummary(result) ?? firstLine(resultContentText(result));
42
17
  return new Text(theme.fg("dim", summary), 0, 0);
43
18
  }
package/src/tool.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * (R2 实测 2026-08-22:tool execute 内 await ctx.compact() 不可行——AgentSession.compact
6
6
  * 开头的 abort() 会中止当前 agent 循环,挂起的 Promise 永不兑现、session 无 toolResult。
7
7
  * 故走 §3.2 降级态:execute 立即返回"压缩已启动",onComplete/onError 后经
8
- * sendSmartContextNotice 注入结果消息(nextTurn 车道,D4①——随下一次 prompt 注入,
8
+ * sendSmartContextNotice 注入结果消息(nextTurn 投递路径,D4①——随下一次 prompt 注入,
9
9
  * 不自起 run;此刻无进行中回合,compact 内部 abort 为 no-op)。
10
10
  * 压缩生成由 session_before_compact 接管 handler 完成(工具只触发,不生成)。
11
11
  *
@@ -153,9 +153,6 @@ export function registerCompactContextTool(
153
153
  pi.registerTool({
154
154
  name: "compact_context",
155
155
  label: "compact_context",
156
- // deferred(tool-declaration-slimming D4):不常驻声明,tool_search 命中即激活;
157
- // 迁移写入的 +tool_search 条目(runtime pi-codemode-settings)承载检索入口
158
- exposure: "deferred",
159
156
  description: TOOL_DESCRIPTION,
160
157
  parameters: CompactContextParams,
161
158
  execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {