@zhushanwen/pi-goal 0.14.10 → 0.14.12

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-goal",
3
- "version": "0.14.10",
3
+ "version": "0.14.12",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -25,15 +25,15 @@
25
25
  "index.ts"
26
26
  ],
27
27
  "peerDependencies": {
28
- "@earendil-works/pi-ai": "^0.84.4",
29
- "@earendil-works/pi-coding-agent": "^0.84.4",
30
- "@earendil-works/pi-tui": "^0.84.4",
31
- "typebox": "*",
32
- "@zhushanwen/pi-pending-notifications": "0.8.1"
28
+ "@earendil-works/pi-ai": "^1.0.0",
29
+ "@earendil-works/pi-coding-agent": "^1.0.0",
30
+ "@earendil-works/pi-tui": "^1.0.0",
31
+ "typebox": "1.3.27",
32
+ "@zhushanwen/pi-pending-notifications": "0.8.3"
33
33
  },
34
34
  "dependencies": {
35
- "@zhushanwen/extension-protocol": "0.17.0",
36
- "@zhushanwen/pi-extension-logger": "0.6.1",
35
+ "@zhushanwen/extension-protocol": "0.19.0",
36
+ "@zhushanwen/pi-extension-logger": "0.6.2",
37
37
  "@zhushanwen/pi-session-path": "0.1.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * message_end handler 测试(D13① role 过滤 + FR-6.7 ESC 守卫)。
3
+ *
4
+ * 断言面:仅 user/assistant 的 message_end 进入 service 消费;system(pi 1.0
5
+ * 新形态)/ toolResult / 缺 role 在 handler 层显式拒绝;assistant 正常透传。
6
+ */
7
+
8
+ import { beforeEach, describe, expect, it, vi } from "vitest";
9
+
10
+ import { handleMessageEnd } from "../adapters/event-handlers/message-end";
11
+ import { createGoalState } from "../engine/goal";
12
+ import { applyEvent } from "../service";
13
+ import { createGoalSession } from "../session";
14
+
15
+ vi.mock("../service", async (importOriginal) => {
16
+ const actual = await importOriginal<typeof import("../service")>();
17
+ return { ...actual, applyEvent: vi.fn(actual.applyEvent) };
18
+ });
19
+
20
+ function makeSession(): ReturnType<typeof createGoalSession> {
21
+ const session = createGoalSession();
22
+ session.state = createGoalState("obj");
23
+ return session;
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.mocked(applyEvent).mockClear();
28
+ });
29
+
30
+ function ctx(signalAborted = false): Parameters<typeof handleMessageEnd>[1] {
31
+ return { signal: { aborted: signalAborted } } as unknown as Parameters<typeof handleMessageEnd>[1];
32
+ }
33
+
34
+ function messageEndEvent(role: unknown): Parameters<typeof handleMessageEnd>[2] {
35
+ return { type: "message_end", message: { role } } as unknown as Parameters<typeof handleMessageEnd>[2];
36
+ }
37
+
38
+ describe("handleMessageEnd(D13① role 过滤)", () => {
39
+ it("assistant:透传 applyEvent(token 累加路径不受过滤影响)", async () => {
40
+ const session = makeSession();
41
+ const event = {
42
+ type: "message_end",
43
+ message: { role: "assistant", usage: { input: 100, output: 50, cacheRead: 20 } },
44
+ } as unknown as Parameters<typeof handleMessageEnd>[2];
45
+ await handleMessageEnd(session, ctx(), event);
46
+
47
+ // 加权口径同 service.test.ts:100×1 + 20×0.02 + 50×2 = 200.4
48
+ expect(session.state?.tokensUsed).toBe(200.4);
49
+ });
50
+
51
+ it("user:进入消费路径(applyEvent 被调;token 累加由 service 层 assistant-only 收窄)", async () => {
52
+ const session = makeSession();
53
+ const spy = vi.mocked(applyEvent);
54
+ await handleMessageEnd(session, ctx(), messageEndEvent("user"));
55
+
56
+ expect(spy).toHaveBeenCalledWith(session, "message_end", messageEndEvent("user"));
57
+ });
58
+
59
+ it("system(pi 1.0 新形态):handler 层拒绝,不进 applyEvent", async () => {
60
+ const session = makeSession();
61
+ const spy = vi.mocked(applyEvent);
62
+ await handleMessageEnd(session, ctx(), messageEndEvent("system"));
63
+
64
+ expect(spy).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it("toolResult / 缺 role:同样拒绝", async () => {
68
+ const session = makeSession();
69
+ const spy = vi.mocked(applyEvent);
70
+ await handleMessageEnd(session, ctx(), messageEndEvent("toolResult"));
71
+ await handleMessageEnd(session, ctx(), {} as Parameters<typeof handleMessageEnd>[2]);
72
+
73
+ expect(spy).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it("ESC 守卫先于 role 过滤(aborted 直接返回)", async () => {
77
+ const session = makeSession();
78
+ const spy = vi.mocked(applyEvent);
79
+ await handleMessageEnd(session, ctx(true), messageEndEvent("assistant"));
80
+
81
+ expect(spy).not.toHaveBeenCalled();
82
+ });
83
+ });
@@ -4,6 +4,12 @@
4
4
  * FR-6.7 ESC 守卫:ctx.signal.aborted 时跳过 token 累加。
5
5
  * FR-8.6: token 累加算法(委托 service.applyEvent("message_end"))。
6
6
  *
7
+ * role 过滤(D13①,rename-session 先例同形态):仅 user/assistant 的 message_end
8
+ * 进入消费——pi 1.0 起追加 system 消息(工具集变更高频写入)与嵌套形态,goal 的
9
+ * 事件消费是对话回合语义,系统/工具结果/扩展形态的 message_end 显式不消费。
10
+ * (token 累加在 service 层另有 assistant-only 收窄,本层过滤是消费关系显式化,
11
+ * 不依赖下游巧合兜住。)
12
+ *
7
13
  * 不 persist / updateWidget。
8
14
  */
9
15
 
@@ -12,6 +18,19 @@ import type { ExtensionContext, MessageEndEvent } from "@earendil-works/pi-codin
12
18
  import { applyEvent } from "../../service";
13
19
  import type { GoalSession } from "../../session";
14
20
 
21
+ /**
22
+ * 畸形防御读取 message.role(D13①):pi 事件经 JSON 解析,message 缺失 / 非对象 /
23
+ * 缺 role 的畸形形态不得崩 handler——一律按「非对话轴形态」拒绝(测试契约:
24
+ * toolResult / 缺 role / 缺 message 同走拒绝)。typeof + in 收窄替代 as 全可选
25
+ * 结构断言(后者任何对象都能通过,等于无校验)。
26
+ */
27
+ function readMessageRole(event: MessageEndEvent): unknown {
28
+ const message: unknown = event.message;
29
+ if (typeof message !== "object" || message === null) return undefined;
30
+ if (!("role" in message)) return undefined;
31
+ return message.role;
32
+ }
33
+
15
34
  export async function handleMessageEnd(
16
35
  session: GoalSession,
17
36
  ctx: ExtensionContext,
@@ -21,5 +40,9 @@ export async function handleMessageEnd(
21
40
  // FR-6.7 ESC 守卫
22
41
  if (ctx.signal?.aborted) return;
23
42
 
43
+ // D13① role 过滤:仅对话轴两形态(AgentMessage role 域为开放联合,宽比较)
44
+ const role = readMessageRole(event);
45
+ if (role !== "user" && role !== "assistant") return;
46
+
24
47
  applyEvent(session, "message_end", event);
25
48
  }
package/src/index.ts CHANGED
@@ -140,7 +140,7 @@ export default function goalExtension(pi: ExtensionAPI) {
140
140
  *
141
141
  * 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 守卫
142
142
  * 读取 `globalThis[Symbol.for("@zhushanwen/pi-goal.goalInit")]`(plan/src/compact.ts)。
143
- * 不挂 pi API 对象:pi 0.84.4 为每个扩展创建独立 ExtensionAPI,挂 pi 对象的字段
143
+ * 不挂 pi API 对象:pi 为每个扩展创建独立 ExtensionAPI(1.0.0 复核同构),挂 pi 对象的字段
144
144
  * 跨扩展不可见。
145
145
  *
146
146
  * @param objective 目标描述