@zhushanwen/pi-agent-ext 1.2.2 → 1.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 +1 -1
- package/src/__tests__/agent-ext.test.ts +81 -21
- package/src/index.ts +39 -18
package/package.json
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 复用 system-prompt-trace index-wiring.test.ts 的 Proxy 假体模式:
|
|
5
5
|
* - pi 用 Proxy 假体:捕获 registerCommand 注册的命令定义 + appendEntry 落点
|
|
6
|
-
* - ctx 只需 index.ts 实际消费的字段(
|
|
6
|
+
* - ctx 只需 index.ts 实际消费的字段(getSystemPrompt / navigateTree),
|
|
7
7
|
* 以 ExtensionCommandContext 最小形状驱动 handler(SDK 双参契约 (args, ctx))
|
|
8
8
|
* - index.ts 对 @earendil-works/pi-coding-agent 是 type-only import(运行时擦除),
|
|
9
9
|
* 无需 vi.mock SDK 模块
|
|
10
10
|
*
|
|
11
11
|
* 锚定两命令注册面 + handler 行为:
|
|
12
|
-
* - __taiji_reload__(host 触发的 skill/extension 重载内部命令)→ ctx.reload()
|
|
13
12
|
* - __taiji_get_system_prompt__(Trace 视图「现取当前值」通道)→ pi.appendEntry
|
|
14
13
|
* 写 taiji:current-system-prompt custom entry(fullText/charCount/fetchedAt 形状)
|
|
14
|
+
* - __taiji_nav__(消息撤回信令,ADR-0076 D1)→ handler 以显式 await 把
|
|
15
|
+
* ctx.navigateTree(entryId, {summarize:false, label:'taiji:revoked'}) 的 promise
|
|
16
|
+
* 纳入自身返回链——写法契约判别断言(deferred sentinel 时序判别):handler 返回
|
|
17
|
+
* promise(非 undefined)且其 settle 严格晚于 navigateTree 的 sentinel settle
|
|
18
|
+
* (fire-and-forget 花括号体:同步体返回 undefined / async 无 await 体先 settle,
|
|
19
|
+
* 两违禁形态均在断言处红)
|
|
15
20
|
*
|
|
16
21
|
* [HISTORICAL] session tree 导航命令(旧品牌时期命名)及其测试随命令删除一并移除
|
|
17
|
-
* (2026-08-31,桥接对端 runtime 消费端已在 monorepo
|
|
22
|
+
* (2026-08-31,桥接对端 runtime 消费端已在 monorepo 时代删除);随消息撤回设计
|
|
23
|
+
* (ADR-0076)以 __taiji_nav__ 形态回归,本文件同步锚定其注册面与写法契约。
|
|
24
|
+
* [HISTORICAL] __taiji_reload__ 及其测试随 W5 skill 变更→pi reload 编排退役一并移除
|
|
25
|
+
* (2026-09-25,消费端已 D7 切源 taiji SkillRegistry,pi 侧列表滞后归 ADR-0050 降级)。
|
|
18
26
|
*
|
|
19
27
|
* 运行:cd extensions/taiji/agent-ext && npx vitest run
|
|
20
28
|
*/
|
|
@@ -27,7 +35,10 @@ import type {
|
|
|
27
35
|
|
|
28
36
|
interface RecordedCommand {
|
|
29
37
|
description: string;
|
|
30
|
-
handler: (
|
|
38
|
+
handler: (
|
|
39
|
+
args: string,
|
|
40
|
+
ctx: ExtensionCommandContext,
|
|
41
|
+
) => Promise<unknown> | void;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
interface RecordedEntry {
|
|
@@ -63,20 +74,20 @@ function createWiringHarness(): WiringHarness {
|
|
|
63
74
|
return { pi, commands, entries };
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
/** ctx 假体(index.ts 实际消费:
|
|
77
|
+
/** ctx 假体(index.ts 实际消费:getSystemPrompt / navigateTree;vi.fn 捕获调用)。 */
|
|
67
78
|
function createCtx(prompt = "current system prompt"): {
|
|
68
79
|
ctx: ExtensionCommandContext;
|
|
69
|
-
reload: ReturnType<typeof vi.fn>;
|
|
70
80
|
getSystemPrompt: ReturnType<typeof vi.fn>;
|
|
81
|
+
navigateTree: ReturnType<typeof vi.fn>;
|
|
71
82
|
} {
|
|
72
|
-
const reload = vi.fn();
|
|
73
83
|
const getSystemPrompt = vi.fn(() => prompt);
|
|
84
|
+
const navigateTree = vi.fn(() => Promise.resolve({ cancelled: false }));
|
|
74
85
|
const ctx = {
|
|
75
86
|
cwd: "/home/user/project",
|
|
76
|
-
reload,
|
|
77
87
|
getSystemPrompt,
|
|
88
|
+
navigateTree,
|
|
78
89
|
} as unknown as ExtensionCommandContext;
|
|
79
|
-
return { ctx,
|
|
90
|
+
return { ctx, getSystemPrompt, navigateTree };
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/** 以 SDK 双参契约 (args, ctx) 驱动已注册命令 handler。 */
|
|
@@ -98,13 +109,13 @@ async function loadExtension(): Promise<(pi: ExtensionAPI) => void> {
|
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
describe("index.ts wiring SDK 契约", () => {
|
|
101
|
-
it("注册恰好两个命令(
|
|
112
|
+
it("注册恰好两个命令(__taiji_get_system_prompt__ / __taiji_nav__),各带非空 description", async () => {
|
|
102
113
|
const ext = await loadExtension();
|
|
103
114
|
const h = createWiringHarness();
|
|
104
115
|
ext(h.pi);
|
|
105
116
|
expect([...h.commands.keys()].sort()).toEqual([
|
|
106
117
|
"__taiji_get_system_prompt__",
|
|
107
|
-
"
|
|
118
|
+
"__taiji_nav__",
|
|
108
119
|
]);
|
|
109
120
|
for (const def of h.commands.values()) {
|
|
110
121
|
expect(typeof def.description).toBe("string");
|
|
@@ -113,16 +124,6 @@ describe("index.ts wiring SDK 契约", () => {
|
|
|
113
124
|
}
|
|
114
125
|
});
|
|
115
126
|
|
|
116
|
-
it("__taiji_reload__ handler → 调 ctx.reload()(host 触发的 skill/extension 重载,无参)", async () => {
|
|
117
|
-
const ext = await loadExtension();
|
|
118
|
-
const h = createWiringHarness();
|
|
119
|
-
ext(h.pi);
|
|
120
|
-
const { ctx, reload } = createCtx();
|
|
121
|
-
await runCommand(h, "__taiji_reload__", "", ctx);
|
|
122
|
-
expect(reload).toHaveBeenCalledTimes(1);
|
|
123
|
-
expect(reload).toHaveBeenCalledWith();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
127
|
it("__taiji_get_system_prompt__ → appendEntry 写 taiji:current-system-prompt(fullText/charCount/fetchedAt 形状,不写其他 customType)", async () => {
|
|
127
128
|
const ext = await loadExtension();
|
|
128
129
|
const h = createWiringHarness();
|
|
@@ -141,4 +142,63 @@ describe("index.ts wiring SDK 契约", () => {
|
|
|
141
142
|
expect(typeof data.fetchedAt).toBe("string");
|
|
142
143
|
expect(Number.isNaN(Date.parse(String(data.fetchedAt)))).toBe(false);
|
|
143
144
|
});
|
|
145
|
+
|
|
146
|
+
it("__taiji_nav__ → 调 ctx.navigateTree 参数三件套逐字(entryId 直传 / summarize:false / label:'taiji:revoked')", async () => {
|
|
147
|
+
const ext = await loadExtension();
|
|
148
|
+
const h = createWiringHarness();
|
|
149
|
+
ext(h.pi);
|
|
150
|
+
const { ctx, navigateTree } = createCtx();
|
|
151
|
+
await runCommand(h, "__taiji_nav__", "msg-entry-1", ctx);
|
|
152
|
+
|
|
153
|
+
expect(navigateTree).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(navigateTree).toHaveBeenCalledWith("msg-entry-1", {
|
|
155
|
+
summarize: false,
|
|
156
|
+
label: "taiji:revoked",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("__taiji_nav__ handler 把 navigateTree 的 promise 纳入自身返回链(D1 写法契约判别:返回 promise 非 undefined 且 settle 严格晚于 navigateTree——非 fire-and-forget)", async () => {
|
|
161
|
+
const ext = await loadExtension();
|
|
162
|
+
const h = createWiringHarness();
|
|
163
|
+
ext(h.pi);
|
|
164
|
+
// deferred sentinel:可控 resolve 时点。写法契约(D1):pi `await command.handler(...)`
|
|
165
|
+
// 接住的 promise 必须等待 navigateTree 完成——「revokeMessage reply 前树变更+落盘
|
|
166
|
+
// 完成」的保证链。判别两违禁形态:①同步花括号体 fire-and-forget 返回 undefined;
|
|
167
|
+
// ②async 无 await 体返回的 promise 先于 sentinel settle。
|
|
168
|
+
// 注:SDK 0.84.4 RegisteredCommand.handler 钉死 Promise<void>,「直接 return
|
|
169
|
+
// navigateTree promise」形态编译不过(TS2322),故 D1 许可形态为显式 await,判别
|
|
170
|
+
// 断言用 settle 时序而非 promise 引用全等。
|
|
171
|
+
let resolveNav!: (value: { cancelled: boolean }) => void;
|
|
172
|
+
const sentinel = new Promise<{ cancelled: boolean }>((resolve) => {
|
|
173
|
+
resolveNav = resolve;
|
|
174
|
+
});
|
|
175
|
+
const { ctx, navigateTree } = createCtx();
|
|
176
|
+
navigateTree.mockReturnValue(sentinel);
|
|
177
|
+
|
|
178
|
+
const cmd = h.commands.get("__taiji_nav__");
|
|
179
|
+
if (cmd === undefined) throw new Error('command "__taiji_nav__" not registered');
|
|
180
|
+
|
|
181
|
+
const returned = cmd.handler("msg-entry-2", ctx);
|
|
182
|
+
// 违禁形态 ①:fire-and-forget 同步花括号体返回 undefined
|
|
183
|
+
expect(returned).toBeInstanceOf(Promise);
|
|
184
|
+
const returnedPromise = returned as Promise<unknown>;
|
|
185
|
+
let handlerSettled = false;
|
|
186
|
+
void returnedPromise.then(() => {
|
|
187
|
+
handlerSettled = true;
|
|
188
|
+
});
|
|
189
|
+
// 泵微任务:给违禁形态 ②(async 无 await 体)让它的 promise 先 settle 的机会
|
|
190
|
+
await Promise.resolve();
|
|
191
|
+
await Promise.resolve();
|
|
192
|
+
// sentinel 未 resolve 前,handler 的 promise 不得 settle——fire-and-forget 在此红
|
|
193
|
+
expect(handlerSettled).toBe(false);
|
|
194
|
+
|
|
195
|
+
resolveNav({ cancelled: false });
|
|
196
|
+
await returnedPromise;
|
|
197
|
+
expect(handlerSettled).toBe(true);
|
|
198
|
+
expect(navigateTree).toHaveBeenCalledTimes(1);
|
|
199
|
+
expect(navigateTree).toHaveBeenCalledWith("msg-entry-2", {
|
|
200
|
+
summarize: false,
|
|
201
|
+
label: "taiji:revoked",
|
|
202
|
+
});
|
|
203
|
+
});
|
|
144
204
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* taiji extension for Pi — internal commands for the host.
|
|
3
3
|
*
|
|
4
|
-
* Registers `/
|
|
5
|
-
*
|
|
6
|
-
* view fetch-current button.
|
|
4
|
+
* Registers `/__taiji_get_system_prompt__` (Trace view fetch-current button)
|
|
5
|
+
* and `/__taiji_nav__` (session tree rewind for message revoke).
|
|
7
6
|
*
|
|
8
|
-
* [HISTORICAL] The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* [HISTORICAL] The session tree navigation command was removed 2026-08-31:
|
|
8
|
+
* its bridge consumer on the runtime side had been deleted, leaving the
|
|
9
|
+
* command with no reachable caller (ADR-0008). It returns as `__taiji_nav__`
|
|
10
|
+
* under the message-revoke design (ADR-0076, decision D1) — the runtime
|
|
11
|
+
* revoke orchestration is the new consumer.
|
|
12
|
+
* [HISTORICAL] `/__taiji_reload__` was removed (2026-09-25): the W5
|
|
13
|
+
* skill-change→pi-reload orchestration was retired — slash menu and composer
|
|
14
|
+
* injection resolve skills from the taiji SkillRegistry (D7 source switch),
|
|
15
|
+
* so a full pi reload (extension ctx invalidate + clearExtensionCache, which
|
|
16
|
+
* broke background-task completion notifications in the stale module world)
|
|
17
|
+
* bought nothing but stale-pi-side lists, accepted per ADR-0050 degradation.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
16
21
|
|
|
17
22
|
export default function (pi: ExtensionAPI): void {
|
|
18
|
-
// W5: builtin internal reload command. `/__taiji_reload__` 由 host 在 skill 文件变动时
|
|
19
|
-
// 经 client.prompt 发起(不经 LLM),handler 调 pi ctx.reload() 重扫 skill + 重建 runtime。
|
|
20
|
-
// 双下划线前缀 = 内部命令(前端 W4 internal-command-filter 过滤 `/__` 前缀不显示)。
|
|
21
|
-
pi.registerCommand('__taiji_reload__', {
|
|
22
|
-
description: 'Internal: reload skills/extensions/prompts (triggered by host on skill file change)',
|
|
23
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
24
|
-
await ctx.reload();
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
23
|
// [fetch-current-system-prompt] Trace 视图「现取当前值」通道(session-trace design §3.1
|
|
29
24
|
// 失败路径 / D2):pi RPC 无 get_system_prompt 命令、getSystemPrompt() 只在 extension
|
|
30
25
|
// API,且现取不能依赖可禁的留痕包(system-prompt-trace 是 feature tier)——挂本
|
|
@@ -46,4 +41,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
46
41
|
});
|
|
47
42
|
},
|
|
48
43
|
});
|
|
44
|
+
|
|
45
|
+
// [message-revoke D1] 已消费层(消息已注入模型上下文)撤回信令:runtime 撤回编排经
|
|
46
|
+
// 系统信令入口直达 client.prompt('/__taiji_nav__ <entryId>')——command 同步执行、
|
|
47
|
+
// 不进模型、无 turn(pi agent-session _tryExecuteExtensionCommand)。entryId 从命令
|
|
48
|
+
// args 取(pi 切首个空格后的原样余串,单空格形态下精确等于 entryId——pi-semantics
|
|
49
|
+
// PS-63 实锚:dist/core/agent-session.js:956-958)。
|
|
50
|
+
// 双下划线前缀 = 内部命令(前端 internal-command-filter 过滤 `/__` 前缀不显示)。
|
|
51
|
+
// summarize:false = 不触发 tree summary;label:'taiji:revoked' = pi 追加 LabelEntry
|
|
52
|
+
// 落盘(树重放规则 leaf=文件尾,重启/空闲回收后回退不复活——pi-semantics PS-60 实锚:
|
|
53
|
+
// dist/core/session-manager.js:673 _buildIndex 文件序循环置 leafId=entry.id、:616
|
|
54
|
+
// _setSessionFile 加载路径重放;navigateTree 回退 = branch() 移指针 + appendLabelChange
|
|
55
|
+
// 落锚文件尾两步,dist/core/agent-session.js:2464)。
|
|
56
|
+
// 写法契约(D1 硬要求):handler 必须把 navigateTree 的 promise 纳入自身返回链——
|
|
57
|
+
// pi `await command.handler(...)` 接住返回值是「revokeMessage reply 前树变更+落盘
|
|
58
|
+
// 完成」的保证链。取 D1 许可形态「显式 await」而非「直接 return」:SDK 0.84.4
|
|
59
|
+
// RegisteredCommand.handler 钉死 `(args, ctx) => Promise<void>`,直接 return
|
|
60
|
+
// navigateTree 的 `Promise<{cancelled}>` 编译不过(TS2322,as 断言亦被拒/禁)。
|
|
61
|
+
// fire-and-forget(去掉 await)会使 reply 先于树变更,产生偶发假阴性 nav-failed——
|
|
62
|
+
// 禁止。
|
|
63
|
+
pi.registerCommand('__taiji_nav__', {
|
|
64
|
+
description:
|
|
65
|
+
'Internal: rewind the session tree to before the target entry (host-triggered for message revoke)',
|
|
66
|
+
handler: async (entryId: string, ctx: ExtensionCommandContext) => {
|
|
67
|
+
await ctx.navigateTree(entryId, { summarize: false, label: 'taiji:revoked' });
|
|
68
|
+
},
|
|
69
|
+
});
|
|
49
70
|
}
|