@zhushanwen/pi-smart-context 0.3.9 → 0.4.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/package.json +8 -3
- package/src/__tests__/summary.test.ts +123 -0
- package/src/render-result.ts +43 -0
- package/src/tool.ts +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-smart-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"pi-package"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@zhushanwen/extension-protocol": "0.21.0",
|
|
39
40
|
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
40
41
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
41
42
|
"@zhushanwen/pi-llm-shared": "0.11.3"
|
|
@@ -51,15 +52,19 @@
|
|
|
51
52
|
"vitest.config.ts"
|
|
52
53
|
],
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
55
55
|
"@earendil-works/pi-ai": "^1.0.0",
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
57
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
56
58
|
"typebox": "1.3.27"
|
|
57
59
|
},
|
|
58
60
|
"peerDependenciesMeta": {
|
|
61
|
+
"@earendil-works/pi-ai": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
59
64
|
"@earendil-works/pi-coding-agent": {
|
|
60
65
|
"optional": true
|
|
61
66
|
},
|
|
62
|
-
"@earendil-works/pi-
|
|
67
|
+
"@earendil-works/pi-tui": {
|
|
63
68
|
"optional": true
|
|
64
69
|
},
|
|
65
70
|
"typebox": {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// compact_context 收起态摘要测试:buildCompactContextSummary 文案 + execute 产出的
|
|
2
|
+
// details.summary + renderResult 折叠/展开形态。mock 形态沿用 tool.test.ts。
|
|
3
|
+
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
// mock 配置加载(不落盘,形态对齐 tool.test.ts)
|
|
8
|
+
vi.mock("../pure.js", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import("../pure.js")>();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
loadSmartContextConfig: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
import { DEFAULT_SMART_CONTEXT_CONFIG, loadSmartContextConfig } from "../pure.js";
|
|
17
|
+
import { buildCompactContextSummary, registerCompactContextTool } from "../tool.js";
|
|
18
|
+
import { renderCompactContextResult } from "../render-result.js";
|
|
19
|
+
|
|
20
|
+
const mockedLoad = vi.mocked(loadSmartContextConfig);
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockedLoad.mockReturnValue({ ...DEFAULT_SMART_CONTEXT_CONFIG });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
interface RegisteredTool { // oe-exempt:20261017:test:测试 harness 契约——断言面引用其字段类型,非抽象接口
|
|
27
|
+
name: string;
|
|
28
|
+
execute: (
|
|
29
|
+
id: string,
|
|
30
|
+
params: Record<string, unknown>,
|
|
31
|
+
signal: undefined,
|
|
32
|
+
onUpdate: undefined,
|
|
33
|
+
ctx: ExtensionContext,
|
|
34
|
+
) => Promise<{ content: Array<{ type: string; text: string }>; details: Record<string, unknown> }>;
|
|
35
|
+
renderResult: (result: unknown, options: { expanded: boolean }, theme: Theme) => { content: unknown };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makePi() {
|
|
39
|
+
const tools: RegisteredTool[] = [];
|
|
40
|
+
const pi = {
|
|
41
|
+
registerTool: (t: RegisteredTool) => tools.push(t),
|
|
42
|
+
sendMessage: vi.fn(),
|
|
43
|
+
sendUserMessage: vi.fn(),
|
|
44
|
+
} as unknown as ExtensionAPI;
|
|
45
|
+
return { pi, tools };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeCtx(): ExtensionContext {
|
|
49
|
+
return {
|
|
50
|
+
model: { provider: "zai", id: "glm" },
|
|
51
|
+
getContextUsage: () => ({ tokens: 450_000, contextWindow: 1_000_000 }),
|
|
52
|
+
sessionManager: { getEntries: () => [] },
|
|
53
|
+
compact: () => {},
|
|
54
|
+
} as unknown as ExtensionContext;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── buildCompactContextSummary ───────────────────────
|
|
58
|
+
|
|
59
|
+
describe("buildCompactContextSummary", () => {
|
|
60
|
+
it("恒为 Context compacted(R2 降级态:返回时压缩未完成,无比例数据)", () => {
|
|
61
|
+
expect(buildCompactContextSummary()).toBe("Context compacted");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── execute 产出的 details.summary ───────────────────
|
|
66
|
+
|
|
67
|
+
describe("compact_context execute details.summary", () => {
|
|
68
|
+
it("details 带现有字段 + summary", async () => {
|
|
69
|
+
const { pi, tools } = makePi();
|
|
70
|
+
registerCompactContextTool(pi);
|
|
71
|
+
const res = await tools[0]!.execute("t1", {}, undefined, undefined, makeCtx());
|
|
72
|
+
expect(res.details).toMatchObject({
|
|
73
|
+
mode: "same-model",
|
|
74
|
+
launched: true,
|
|
75
|
+
fellBack: false,
|
|
76
|
+
summary: "Context compacted",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── renderCompactContextResult(TUI renderResult)────
|
|
82
|
+
|
|
83
|
+
const mockTheme = {
|
|
84
|
+
fg: (_color: string, text: string) => text,
|
|
85
|
+
bg: (_color: string, text: string) => text,
|
|
86
|
+
bold: (text: string) => text,
|
|
87
|
+
italic: (text: string) => text,
|
|
88
|
+
underline: (text: string) => text,
|
|
89
|
+
inverse: (text: string) => text,
|
|
90
|
+
strikethrough: (text: string) => text,
|
|
91
|
+
getFgAnsi: (_color: string) => "",
|
|
92
|
+
getBgAnsi: (_color: string) => "",
|
|
93
|
+
getColorMode: () => "truecolor" as const,
|
|
94
|
+
getThinkingBorderColor: () => (text: string) => text,
|
|
95
|
+
getBashModeBorderColor: () => (text: string) => text,
|
|
96
|
+
} as unknown as Theme;
|
|
97
|
+
|
|
98
|
+
function renderedText(result: unknown, expanded: boolean): string {
|
|
99
|
+
return renderCompactContextResult(result, { expanded }, mockTheme)
|
|
100
|
+
.render(400)
|
|
101
|
+
.map((line) => line.trimEnd())
|
|
102
|
+
.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("renderCompactContextResult", () => {
|
|
106
|
+
const result = {
|
|
107
|
+
content: [{ type: "text", text: "压缩已启动(same-model 模式)..." }],
|
|
108
|
+
details: { mode: "same-model", launched: true, summary: "Context compacted" },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
it("折叠态 → details.summary 一行", () => {
|
|
112
|
+
expect(renderedText(result, false)).toBe("Context compacted");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("展开态 → content 纯文本", () => {
|
|
116
|
+
expect(renderedText(result, true)).toContain("压缩已启动");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("无 summary(旧 entry)→ 折叠态回退 content 首行", () => {
|
|
120
|
+
const legacy = { content: [{ type: "text", text: "压缩已启动(same-model 模式)" }], details: {} };
|
|
121
|
+
expect(renderedText(legacy, false)).toBe("压缩已启动(same-model 模式)");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact_context 工具的 TUI renderResult(折叠态一行摘要 / 展开态 content 纯文本)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
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
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 折叠态(expanded=false)= details.summary 一行(dim);展开态 = content 纯文本。 */
|
|
35
|
+
export function renderCompactContextResult(
|
|
36
|
+
result: unknown,
|
|
37
|
+
options: { expanded: boolean },
|
|
38
|
+
theme: Theme,
|
|
39
|
+
): Text {
|
|
40
|
+
if (options.expanded) return new Text(contentText(result), 0, 0);
|
|
41
|
+
const summary = readDetailsSummary(result) ?? firstLine(contentText(result));
|
|
42
|
+
return new Text(theme.fg("dim", summary), 0, 0);
|
|
43
|
+
}
|
package/src/tool.ts
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
* 「manual + 非 runtime 发起 + compaction_start 前 turn 被掐断」后经投递内核追加续跑投递。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { guardStaleCtx, isRecord, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
19
19
|
import { Type } from "typebox";
|
|
20
20
|
|
|
21
21
|
import { debugLog } from "./compact-handler.js";
|
|
22
22
|
import { sendSmartContextNotice } from "./notices.js";
|
|
23
|
+
import { renderCompactContextResult } from "./render-result.js";
|
|
23
24
|
import { buildDegradationHintLine } from "./reminder.js";
|
|
24
25
|
import {
|
|
25
26
|
DEGRADATION_HINT_MIN_COMPACTIONS,
|
|
@@ -63,6 +64,16 @@ export interface CompactContextDetails {
|
|
|
63
64
|
/** 压缩已触发(结果经注入消息送达,不在工具结果里)。 */
|
|
64
65
|
launched: boolean;
|
|
65
66
|
fellBack: boolean;
|
|
67
|
+
/** 收起态一行摘要(buildCompactContextSummary 产出;GUI 收起态与 TUI 折叠态共用)。 */
|
|
68
|
+
summary?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 收起态一行摘要。R2 降级态下工具返回时压缩尚未完成(结果异步注入),无保留比例
|
|
73
|
+
* 数据可取——恒为 "Context compacted";完成后的比例数据走注入消息,不进本摘要。
|
|
74
|
+
*/
|
|
75
|
+
export function buildCompactContextSummary(): string {
|
|
76
|
+
return "Context compacted";
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
/** 工具描述(§3.5:三条件自查引导;条件即纪律,无第三层复述——D1 单源化核对通过)。 */
|
|
@@ -236,8 +247,13 @@ export function registerCompactContextTool(
|
|
|
236
247
|
fellBack: false,
|
|
237
248
|
compactionCount: compactionCount + 1,
|
|
238
249
|
launched: true,
|
|
250
|
+
summary: buildCompactContextSummary(),
|
|
239
251
|
} satisfies CompactContextDetails,
|
|
240
252
|
};
|
|
241
253
|
},
|
|
254
|
+
|
|
255
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
256
|
+
return renderCompactContextResult(result, options, theme);
|
|
257
|
+
},
|
|
242
258
|
});
|
|
243
259
|
}
|