@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-agent-ext",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "taiji extension for Pi — internal host commands for extension reload and system prompt fetch",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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 实际消费的字段(reload / getSystemPrompt),
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: (args: string, ctx: ExtensionCommandContext) => Promise<void> | void;
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 实际消费:reload / getSystemPrompt;vi.fn 捕获调用)。 */
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, reload, getSystemPrompt };
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("注册恰好两个命令(__taiji_reload__ / __taiji_get_system_prompt__),各带非空 description", async () => {
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
- "__taiji_reload__",
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 `/__taiji_reload__` internal command for host-triggered
5
- * skill/extension reload, and `/__taiji_get_system_prompt__` for the Trace
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 former session tree navigation command was removed
9
- * (2026-08-31): its bridge consumer on the runtime side was deleted in the
10
- * monorepo era, so the command had no reachable caller. See the ADR-0008
11
- * note in the retired-decisions genealogy of docs/adr/decisions.md for the
12
- * original design.
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
  }