@zhushanwen/pi-agent-ext 1.0.1 → 1.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/package.json +6 -2
- package/src/__tests__/agent-ext.test.ts +166 -6
- package/src/index.ts +26 -2
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-agent-ext",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "xyz-agent extension for Pi — session tree navigation and internal reload command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"xyz-agent": {
|
|
8
|
+
"role": "taiji"
|
|
9
|
+
},
|
|
7
10
|
"pi": {
|
|
8
11
|
"extensions": [
|
|
9
12
|
"./index.ts"
|
|
@@ -22,10 +25,11 @@
|
|
|
22
25
|
"index.ts"
|
|
23
26
|
],
|
|
24
27
|
"peerDependencies": {
|
|
25
|
-
"@earendil-works/pi-coding-agent": "
|
|
28
|
+
"@earendil-works/pi-coding-agent": "^0.84.1"
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"@types/node": "^24.0.0",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
29
33
|
"vitest": "^4.1.8"
|
|
30
34
|
},
|
|
31
35
|
"scripts": {
|
|
@@ -1,7 +1,167 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* index.ts wiring SDK 契约测试(round3 review:替换占位测试)。
|
|
3
|
+
*
|
|
4
|
+
* 复用 system-prompt-trace index-wiring.test.ts 的 Proxy 假体模式:
|
|
5
|
+
* - pi 用 Proxy 假体:捕获 registerCommand 注册的命令定义 + appendEntry 落点
|
|
6
|
+
* - ctx 只需 index.ts 实际消费的字段(reload / navigateTree / getSystemPrompt),
|
|
7
|
+
* 以 ExtensionCommandContext 最小形状驱动 handler(SDK 双参契约 (args, ctx))
|
|
8
|
+
* - index.ts 对 @earendil-works/pi-coding-agent 是 type-only import(运行时擦除),
|
|
9
|
+
* 无需 vi.mock SDK 模块
|
|
10
|
+
*
|
|
11
|
+
* 锚定三命令注册面 + handler 行为:
|
|
12
|
+
* - __xyz_reload__(host 触发的 skill/extension 重载内部命令)→ ctx.reload()
|
|
13
|
+
* - xyz-navigate(session tree 导航)→ 空 entryId 早退;有效 entryId →
|
|
14
|
+
* ctx.navigateTree(entryId, { summarize: false })
|
|
15
|
+
* - __xyz_get_system_prompt__(Trace 视图「现取当前值」通道)→ pi.appendEntry
|
|
16
|
+
* 写 xyz:current-system-prompt custom entry(fullText/charCount/fetchedAt 形状)
|
|
17
|
+
*
|
|
18
|
+
* 运行:cd extensions/taiji/agent-ext && npx vitest run
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
21
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
22
|
+
import type {
|
|
23
|
+
ExtensionAPI,
|
|
24
|
+
ExtensionCommandContext,
|
|
25
|
+
} from "@earendil-works/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
interface RecordedCommand {
|
|
28
|
+
description: string;
|
|
29
|
+
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> | void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RecordedEntry {
|
|
33
|
+
customType: string;
|
|
34
|
+
data: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WiringHarness {
|
|
38
|
+
pi: ExtensionAPI;
|
|
39
|
+
commands: Map<string, RecordedCommand>;
|
|
40
|
+
entries: RecordedEntry[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Proxy 假体 pi:捕获 registerCommand 注册面与 appendEntry 落点(其余成员 no-op)。 */
|
|
44
|
+
function createWiringHarness(): WiringHarness {
|
|
45
|
+
const commands = new Map<string, RecordedCommand>();
|
|
46
|
+
const entries: RecordedEntry[] = [];
|
|
47
|
+
const pi = new Proxy<ExtensionAPI>({} as ExtensionAPI, {
|
|
48
|
+
get(_target: unknown, prop: string | symbol): unknown {
|
|
49
|
+
if (prop === "registerCommand") {
|
|
50
|
+
return (name: string, def: RecordedCommand): void => {
|
|
51
|
+
commands.set(name, def);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (prop === "appendEntry") {
|
|
55
|
+
return (customType: string, data?: unknown): void => {
|
|
56
|
+
entries.push({ customType, data });
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return (): void => undefined;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return { pi, commands, entries };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** ctx 假体(index.ts 实际消费:reload / navigateTree / getSystemPrompt;vi.fn 捕获调用)。 */
|
|
66
|
+
function createCtx(prompt = "current system prompt"): {
|
|
67
|
+
ctx: ExtensionCommandContext;
|
|
68
|
+
reload: ReturnType<typeof vi.fn>;
|
|
69
|
+
navigateTree: ReturnType<typeof vi.fn>;
|
|
70
|
+
getSystemPrompt: ReturnType<typeof vi.fn>;
|
|
71
|
+
} {
|
|
72
|
+
const reload = vi.fn();
|
|
73
|
+
const navigateTree = vi.fn();
|
|
74
|
+
const getSystemPrompt = vi.fn(() => prompt);
|
|
75
|
+
const ctx = {
|
|
76
|
+
cwd: "/home/user/project",
|
|
77
|
+
reload,
|
|
78
|
+
navigateTree,
|
|
79
|
+
getSystemPrompt,
|
|
80
|
+
} as unknown as ExtensionCommandContext;
|
|
81
|
+
return { ctx, reload, navigateTree, getSystemPrompt };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 以 SDK 双参契约 (args, ctx) 驱动已注册命令 handler。 */
|
|
85
|
+
async function runCommand(
|
|
86
|
+
h: WiringHarness,
|
|
87
|
+
name: string,
|
|
88
|
+
args: string,
|
|
89
|
+
ctx: ExtensionCommandContext,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const cmd = h.commands.get(name);
|
|
92
|
+
if (cmd === undefined) throw new Error(`command "${name}" not registered`);
|
|
93
|
+
await cmd.handler(args, ctx);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 加载默认导出工厂(wiring 入口)。 */
|
|
97
|
+
async function loadExtension(): Promise<(pi: ExtensionAPI) => void> {
|
|
98
|
+
const mod = await import("../index.js");
|
|
99
|
+
return mod.default;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("index.ts wiring SDK 契约", () => {
|
|
103
|
+
it("注册恰好三个命令(__xyz_reload__ / xyz-navigate / __xyz_get_system_prompt__),各带非空 description", async () => {
|
|
104
|
+
const ext = await loadExtension();
|
|
105
|
+
const h = createWiringHarness();
|
|
106
|
+
ext(h.pi);
|
|
107
|
+
expect([...h.commands.keys()].sort()).toEqual([
|
|
108
|
+
"__xyz_get_system_prompt__",
|
|
109
|
+
"__xyz_reload__",
|
|
110
|
+
"xyz-navigate",
|
|
111
|
+
]);
|
|
112
|
+
for (const def of h.commands.values()) {
|
|
113
|
+
expect(typeof def.description).toBe("string");
|
|
114
|
+
expect(def.description.length).toBeGreaterThan(0);
|
|
115
|
+
expect(typeof def.handler).toBe("function");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("__xyz_reload__ handler → 调 ctx.reload()(host 触发的 skill/extension 重载,无参)", async () => {
|
|
120
|
+
const ext = await loadExtension();
|
|
121
|
+
const h = createWiringHarness();
|
|
122
|
+
ext(h.pi);
|
|
123
|
+
const { ctx, reload } = createCtx();
|
|
124
|
+
await runCommand(h, "__xyz_reload__", "", ctx);
|
|
125
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect(reload).toHaveBeenCalledWith();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("xyz-navigate 空 entryId(空串/纯空白)→ 早退,不调 ctx.navigateTree", async () => {
|
|
130
|
+
const ext = await loadExtension();
|
|
131
|
+
const h = createWiringHarness();
|
|
132
|
+
ext(h.pi);
|
|
133
|
+
const { ctx, navigateTree } = createCtx();
|
|
134
|
+
await runCommand(h, "xyz-navigate", "", ctx);
|
|
135
|
+
await runCommand(h, "xyz-navigate", " ", ctx);
|
|
136
|
+
expect(navigateTree).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("xyz-navigate 有效 entryId → ctx.navigateTree(entryId, { summarize: false })(args 去首尾空白)", async () => {
|
|
140
|
+
const ext = await loadExtension();
|
|
141
|
+
const h = createWiringHarness();
|
|
142
|
+
ext(h.pi);
|
|
143
|
+
const { ctx, navigateTree } = createCtx();
|
|
144
|
+
await runCommand(h, "xyz-navigate", " 0198f-entry-1 ", ctx);
|
|
145
|
+
expect(navigateTree).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(navigateTree).toHaveBeenCalledWith("0198f-entry-1", { summarize: false });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("__xyz_get_system_prompt__ → appendEntry 写 xyz:current-system-prompt(fullText/charCount/fetchedAt 形状,不写其他 customType)", async () => {
|
|
150
|
+
const ext = await loadExtension();
|
|
151
|
+
const h = createWiringHarness();
|
|
152
|
+
ext(h.pi);
|
|
153
|
+
const prompt = "prompt body\nline-1";
|
|
154
|
+
const { ctx, getSystemPrompt } = createCtx(prompt);
|
|
155
|
+
await runCommand(h, "__xyz_get_system_prompt__", "", ctx);
|
|
156
|
+
|
|
157
|
+
expect(getSystemPrompt).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(h.entries).toHaveLength(1);
|
|
159
|
+
expect(h.entries[0]?.customType).toBe("xyz:current-system-prompt");
|
|
160
|
+
const data = h.entries[0]?.data as Record<string, unknown>;
|
|
161
|
+
expect(data.fullText).toBe(prompt);
|
|
162
|
+
expect(data.charCount).toBe(prompt.length);
|
|
163
|
+
// fetchedAt = new Date().toISOString()(动态值,断言 ISO 可解析形状)
|
|
164
|
+
expect(typeof data.fetchedAt).toBe("string");
|
|
165
|
+
expect(Number.isNaN(Date.parse(String(data.fetchedAt)))).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* `ctx.navigateTree()` to move the session leaf.
|
|
6
6
|
*
|
|
7
7
|
* Also registers `/__xyz_reload__` internal command for host-triggered
|
|
8
|
-
* skill/extension reload
|
|
8
|
+
* skill/extension reload, and `/__xyz_get_system_prompt__` for the Trace
|
|
9
|
+
* view fetch-current button.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -30,4 +31,27 @@ export default function (pi: ExtensionAPI): void {
|
|
|
30
31
|
await ctx.navigateTree(entryId, { summarize: false });
|
|
31
32
|
},
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
// [fetch-current-system-prompt] Trace 视图「现取当前值」通道(session-trace design §3.1
|
|
36
|
+
// 失败路径 / D2):pi RPC 无 get_system_prompt 命令、getSystemPrompt() 只在 extension
|
|
37
|
+
// API,且现取不能依赖可禁的留痕包(system-prompt-trace 是 feature tier)——挂本
|
|
38
|
+
// infrastructure 包(builtin 不可禁,原根目录 xyz-agent-extension.js 常驻文件随 main
|
|
39
|
+
// 的 agent-ext 包化迁移至此)。host 经 client.prompt 发 /__xyz_get_system_prompt__
|
|
40
|
+
// (双下划线 = 内部命令,前端过滤 /__ 前缀不显示),handler 取当前 system prompt 写
|
|
41
|
+
// xyz:current-system-prompt custom entry(不进 LLM context,零模型侧影响——pi
|
|
42
|
+
// sessionEntryToContextMessages 对 type=custom 落入末尾 return [],session-manager.ts:383-413),
|
|
43
|
+
// runtime 轮询 get_entries(since) 拉到后返回前端(同条 entry 也会作为 DATA 行出现在 trace
|
|
44
|
+
// 台账里,留下取值痕迹)。
|
|
45
|
+
pi.registerCommand("__xyz_get_system_prompt__", {
|
|
46
|
+
description:
|
|
47
|
+
"Internal: append a custom entry with the current system prompt (host-initiated for the Trace view fetch-current button)",
|
|
48
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
49
|
+
const fullText = ctx.getSystemPrompt();
|
|
50
|
+
pi.appendEntry("xyz:current-system-prompt", {
|
|
51
|
+
fullText,
|
|
52
|
+
charCount: fullText.length,
|
|
53
|
+
fetchedAt: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|