@zhushanwen/pi-goal 0.14.9 → 0.14.11
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.
|
|
3
|
+
"version": "0.14.11",
|
|
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,16 @@
|
|
|
25
25
|
"index.ts"
|
|
26
26
|
],
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": "^0.
|
|
29
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
30
|
-
"@earendil-works/pi-tui": "^0.
|
|
31
|
-
"typebox": "
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.8.
|
|
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.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhushanwen/extension-protocol": "0.
|
|
36
|
-
"@zhushanwen/pi-extension-logger": "0.6.
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.18.0",
|
|
36
|
+
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
37
|
+
"@zhushanwen/pi-session-path": "0.1.0"
|
|
37
38
|
},
|
|
38
39
|
"peerDependenciesMeta": {
|
|
39
40
|
"@earendil-works/pi-tui": {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U6c:reconstructGoalState 重建接活跃路径裁剪 + session_tree handler 即时重建。
|
|
3
|
+
*
|
|
4
|
+
* 从 leafId 沿 parentId 回溯得活跃路径,被撤子树的 goal-state entry 不进重建态
|
|
5
|
+
* (不得经 before_agent_start 逐轮注入模型上下文——A14)。
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { createGoalState } from "../engine/goal";
|
|
11
|
+
import { ENTRY_TYPE, serializeState } from "../persistence";
|
|
12
|
+
import type { SessionEntryLike } from "../ports";
|
|
13
|
+
import goalExtension from "../index";
|
|
14
|
+
import {
|
|
15
|
+
createGoalSession,
|
|
16
|
+
reconstructGoalState,
|
|
17
|
+
type ActivePathSessionView,
|
|
18
|
+
} from "../session";
|
|
19
|
+
|
|
20
|
+
// ── fixture ─────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface BranchEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
parentId: string | null;
|
|
25
|
+
type: string;
|
|
26
|
+
customType?: string;
|
|
27
|
+
data?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** goal-state 快照 entry(append-only 全量快照,reconstruct 只读活跃路径内最新一条) */
|
|
31
|
+
function goalStateEntry(id: string, parentId: string | null, objective: string): BranchEntry {
|
|
32
|
+
return {
|
|
33
|
+
type: "custom",
|
|
34
|
+
id,
|
|
35
|
+
parentId,
|
|
36
|
+
customType: ENTRY_TYPE,
|
|
37
|
+
data: serializeState(createGoalState(objective)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 撤回后真实形态:label entry 落文件尾(parentId = 回退后叶子),leafId 指向它 */
|
|
42
|
+
function labelEntry(id: string, parentId: string | null): BranchEntry {
|
|
43
|
+
return { type: "label", id, parentId, targetId: "u-x", label: "taiji:revoked" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeView(entries: SessionEntryLike[], leafId: string | null): ActivePathSessionView {
|
|
47
|
+
return {
|
|
48
|
+
// 模拟 Pi SDK filter-copy 语义:返回新数组
|
|
49
|
+
getEntries: () => entries.slice(),
|
|
50
|
+
getLeafId: () => leafId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── reconstructGoalState 活跃路径裁剪(U6c)──────────
|
|
55
|
+
|
|
56
|
+
describe("reconstructGoalState 活跃路径裁剪(U6c)", () => {
|
|
57
|
+
it("有分支 fixture:被撤子树的 goal-state 不进重建态——取活跃路径内最新一条", () => {
|
|
58
|
+
// 树:e1(goal base) → e2(goal active-branch);e1 → e3(goal revoked-branch,
|
|
59
|
+
// 物理后写 = 被撤子树);撤回落 label 锚 L(parentId = 回退后叶子 e2),
|
|
60
|
+
// leafId = L → 活跃路径 {L, e2, e1}
|
|
61
|
+
const entries = [
|
|
62
|
+
goalStateEntry("e1", null, "base"),
|
|
63
|
+
goalStateEntry("e2", "e1", "active-branch"),
|
|
64
|
+
goalStateEntry("e3", "e1", "revoked-branch"),
|
|
65
|
+
labelEntry("L", "e2"),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const session = createGoalSession();
|
|
69
|
+
reconstructGoalState(session, makeView(entries, "L"));
|
|
70
|
+
|
|
71
|
+
expect(session.state).not.toBeNull();
|
|
72
|
+
expect(session.state!.objective).toBe("active-branch");
|
|
73
|
+
// 反向断言:被撤子树快照不进重建态(物理位置在 e2 之后,无裁剪时会命中)
|
|
74
|
+
expect(session.state!.objective).not.toBe("revoked-branch");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("活跃路径内无 goal-state(撤回点早于 goal 创建)→ state=null,不回退到旧分支快照", () => {
|
|
78
|
+
const entries: BranchEntry[] = [
|
|
79
|
+
{ type: "custom", id: "e1", parentId: null, customType: "other-ext", data: {} },
|
|
80
|
+
goalStateEntry("e2", "e1", "only-on-revoked-branch"),
|
|
81
|
+
labelEntry("L", "e1"),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const session = createGoalSession();
|
|
85
|
+
reconstructGoalState(session, makeView(entries, "L"));
|
|
86
|
+
|
|
87
|
+
expect(session.state).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("无分支回归:leafId = 文件尾 → 恢复最新一条(现行为不变)", () => {
|
|
91
|
+
const entries = [goalStateEntry("e1", null, "old"), goalStateEntry("e2", "e1", "new")];
|
|
92
|
+
|
|
93
|
+
const session = createGoalSession();
|
|
94
|
+
reconstructGoalState(session, makeView(entries, "e2"));
|
|
95
|
+
|
|
96
|
+
expect(session.state!.objective).toBe("new");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("legacy fixture 回归:entry 无 id/parentId(duck-typed 最小形状)→ 线性全量,行为与裁剪前一致", () => {
|
|
100
|
+
const entries: SessionEntryLike[] = [
|
|
101
|
+
{ type: "custom", customType: ENTRY_TYPE, data: serializeState(createGoalState("legacy-old")) },
|
|
102
|
+
{ type: "custom", customType: ENTRY_TYPE, data: serializeState(createGoalState("legacy-new")) },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const session = createGoalSession();
|
|
106
|
+
reconstructGoalState(session, { getEntries: () => entries.slice() });
|
|
107
|
+
|
|
108
|
+
expect(session.state!.objective).toBe("legacy-new");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("leafId 防御:null / 指向不存在 entry → 回退文件尾(不静默清空重建态)", () => {
|
|
112
|
+
const entries = [goalStateEntry("e1", null, "first"), goalStateEntry("e2", "e1", "tail-snapshot")];
|
|
113
|
+
|
|
114
|
+
for (const leafId of [null, "missing-id"]) {
|
|
115
|
+
const session = createGoalSession();
|
|
116
|
+
reconstructGoalState(session, makeView(entries, leafId));
|
|
117
|
+
expect(session.state!.objective).toBe("tail-snapshot");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── session_tree handler(U6c:即时重建,纯重建体)──
|
|
123
|
+
|
|
124
|
+
interface HandlerFixture {
|
|
125
|
+
handlers: Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>;
|
|
126
|
+
sendMessage: ReturnType<typeof vi.fn>;
|
|
127
|
+
appendEntry: ReturnType<typeof vi.fn>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 实例化 goal 工厂并捕获事件 handler(最小 pi:注册面 vi.fn + 副作用面 vi.fn) */
|
|
131
|
+
function makeHandlerFixture(): HandlerFixture {
|
|
132
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
|
|
133
|
+
const pi = {
|
|
134
|
+
registerCommand: vi.fn(),
|
|
135
|
+
registerTool: vi.fn(),
|
|
136
|
+
on: vi.fn((event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<unknown>) => {
|
|
137
|
+
// 真实 pi 的 on 是 handler 列表追加;此处每事件单 handler(goal 工厂每事件只注册一次)
|
|
138
|
+
handlers.set(event, handler);
|
|
139
|
+
}),
|
|
140
|
+
registerMessageRenderer: vi.fn(),
|
|
141
|
+
sendMessage: vi.fn(),
|
|
142
|
+
appendEntry: vi.fn(),
|
|
143
|
+
} as unknown as ExtensionAPI;
|
|
144
|
+
goalExtension(pi);
|
|
145
|
+
return { handlers, sendMessage: pi.sendMessage, appendEntry: pi.appendEntry };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 最小 ctx:sessionManager(分支树)+ ui 记录面 + getContextUsage(before_agent_start 用) */
|
|
149
|
+
function makeCtx(entries: SessionEntryLike[], leafId: string | null) {
|
|
150
|
+
const setWidget = vi.fn();
|
|
151
|
+
const setStatus = vi.fn();
|
|
152
|
+
const ctx = {
|
|
153
|
+
hasUI: true,
|
|
154
|
+
getContextUsage: () => null,
|
|
155
|
+
ui: {
|
|
156
|
+
notify: vi.fn(),
|
|
157
|
+
setStatus,
|
|
158
|
+
setWidget,
|
|
159
|
+
theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
|
|
160
|
+
},
|
|
161
|
+
sessionManager: {
|
|
162
|
+
getEntries: () => entries.slice(),
|
|
163
|
+
getLeafId: () => leafId,
|
|
164
|
+
},
|
|
165
|
+
} as unknown as ExtensionContext;
|
|
166
|
+
return { ctx, setWidget, setStatus };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe("session_tree handler 即时重建(U6c)", () => {
|
|
170
|
+
it("触发 session_tree → 内存态重建为活跃路径态 + widget 刷新;handler 本身零注入零落盘", async () => {
|
|
171
|
+
const { handlers, sendMessage, appendEntry } = makeHandlerFixture();
|
|
172
|
+
const entries = [
|
|
173
|
+
goalStateEntry("e1", null, "base"),
|
|
174
|
+
goalStateEntry("e2", "e1", "active-branch"),
|
|
175
|
+
goalStateEntry("e3", "e1", "revoked-branch"),
|
|
176
|
+
labelEntry("L", "e2"),
|
|
177
|
+
];
|
|
178
|
+
const { ctx, setWidget } = makeCtx(entries, "L");
|
|
179
|
+
|
|
180
|
+
expect(handlers.has("session_tree")).toBe(true);
|
|
181
|
+
await handlers.get("session_tree")!({ type: "session_tree" }, ctx);
|
|
182
|
+
|
|
183
|
+
// 纯重建断言:handler 不注入消息(撤回编排自己不得发消息)、不落盘(不污染新分支文件尾)
|
|
184
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
185
|
+
expect(appendEntry).not.toHaveBeenCalled();
|
|
186
|
+
// widget 反映活跃路径态(mock ctx 无 mode → setWidgetDual 走 TUI 臂,直接推 text 行)
|
|
187
|
+
const lines = setWidget.mock.calls.find(([name]) => name === "goal")?.[1] as string[] | undefined;
|
|
188
|
+
expect(lines?.join(" ")).toContain("active-branch");
|
|
189
|
+
expect(lines?.join(" ")).not.toContain("revoked-branch");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("撤回点早于 goal 创建 → 重建态 null,widget 清除(live 态无残影,A14)", async () => {
|
|
193
|
+
const { handlers } = makeHandlerFixture();
|
|
194
|
+
const entries: BranchEntry[] = [
|
|
195
|
+
{ type: "custom", id: "e1", parentId: null, customType: "other-ext", data: {} },
|
|
196
|
+
goalStateEntry("e2", "e1", "only-on-revoked-branch"),
|
|
197
|
+
labelEntry("L", "e1"),
|
|
198
|
+
];
|
|
199
|
+
const { ctx, setWidget } = makeCtx(entries, "L");
|
|
200
|
+
|
|
201
|
+
await handlers.get("session_tree")!({ type: "session_tree" }, ctx);
|
|
202
|
+
|
|
203
|
+
expect(setWidget).toHaveBeenCalledWith("goal", undefined);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("session_tree 重建后的内存态驱动 before_agent_start:下一轮注入不含被撤 goal", async () => {
|
|
207
|
+
const { handlers, appendEntry } = makeHandlerFixture();
|
|
208
|
+
const entries = [
|
|
209
|
+
goalStateEntry("e1", null, "active-branch"),
|
|
210
|
+
goalStateEntry("e2", "e1", "revoked-branch"),
|
|
211
|
+
labelEntry("L", "e1"),
|
|
212
|
+
];
|
|
213
|
+
const { ctx } = makeCtx(entries, "L");
|
|
214
|
+
|
|
215
|
+
await handlers.get("session_tree")!({ type: "session_tree" }, ctx);
|
|
216
|
+
// before_agent_start 读内存态不重算(现状契约)——新鲜度由 session_tree 重建保证
|
|
217
|
+
const result = (await handlers.get("before_agent_start")!({}, ctx)) as
|
|
218
|
+
| { message: { content: string } }
|
|
219
|
+
| undefined;
|
|
220
|
+
|
|
221
|
+
expect(result?.message?.content).toContain("active-branch");
|
|
222
|
+
expect(result?.message?.content).not.toContain("revoked-branch");
|
|
223
|
+
// before_agent_start 自身的 goal:log 落盘是该事件既有行为,不算 session_tree 副作用
|
|
224
|
+
expect(appendEntry).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -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
|
@@ -29,8 +29,9 @@ import { handleSessionStart } from "./adapters/event-handlers/session-start";
|
|
|
29
29
|
import { handleTurnEnd } from "./adapters/event-handlers/turn-end";
|
|
30
30
|
import { registerGoalControlTool } from "./adapters/goal-control-adapter";
|
|
31
31
|
import { buildPorts } from "./adapters/ports";
|
|
32
|
+
import { updateWidget } from "./projection/widget";
|
|
32
33
|
import { createGoal } from "./service";
|
|
33
|
-
import { createGoalSession, type GoalSession } from "./session";
|
|
34
|
+
import { createGoalSession, reconstructGoalState, type GoalSession } from "./session";
|
|
34
35
|
|
|
35
36
|
// ── Extension Factory ─────────────────────────────────
|
|
36
37
|
|
|
@@ -88,6 +89,21 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
88
89
|
await handleSessionStart(pi, session, ctx);
|
|
89
90
|
});
|
|
90
91
|
|
|
92
|
+
// U6c:树回退(撤回 __taiji_nav__ → navigateTree / 用户树跳转)后即时重建 goal
|
|
93
|
+
// 内存态——before_agent_start 注入读内存态不重算(现状契约),新鲜度由本 handler
|
|
94
|
+
// 保证;缺席时运行中撤回后被撤 goal 逐轮注入模型上下文(A14)。
|
|
95
|
+
// 纯重建体(todo 的 session_tree handler 同款)——只做 reconstruct + widget 刷新,
|
|
96
|
+
// 绝不加 sendMessage / appendEntry 副作用:本事件由撤回编排触发,注入会让撤回编排
|
|
97
|
+
// 自己发出消息(违背「被撤内容从未发生」语义),落盘会向回退后的新分支追加 entry
|
|
98
|
+
// 污染文件尾。
|
|
99
|
+
// updateWidget 无条件调用(session_start 仅 state 非空时刷新——那是启动期无残留可清;
|
|
100
|
+
// 此处撤回点早于 goal 创建时重建态为 null,条件刷新会让被撤 goal 的 widget 残留)。
|
|
101
|
+
pi.on("session_tree", async (_event: unknown, ctx: ExtensionContext) => {
|
|
102
|
+
const ports = buildPorts(pi, ctx);
|
|
103
|
+
reconstructGoalState(session, ctx.sessionManager);
|
|
104
|
+
updateWidget(session, ports.ui);
|
|
105
|
+
});
|
|
106
|
+
|
|
91
107
|
// MF-R2-1 根修:session_shutdown 在 runner invalidate 之前触发(此刻 pi/ctx
|
|
92
108
|
// 仍可用),取消待发的退避 continuation timer——覆盖 reload / new / resume /
|
|
93
109
|
// fork / quit 全部失效路径,旧 timer 不再携 stale ctx 闭包存活到到期
|
|
@@ -124,7 +140,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
124
140
|
*
|
|
125
141
|
* 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 守卫
|
|
126
142
|
* 读取 `globalThis[Symbol.for("@zhushanwen/pi-goal.goalInit")]`(plan/src/compact.ts)。
|
|
127
|
-
* 不挂 pi API 对象:pi 0.
|
|
143
|
+
* 不挂 pi API 对象:pi 为每个扩展创建独立 ExtensionAPI(1.0.0 复核同构),挂 pi 对象的字段
|
|
128
144
|
* 跨扩展不可见。
|
|
129
145
|
*
|
|
130
146
|
* @param objective 目标描述
|
package/src/session.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Session 层 — 运行时句柄 + 状态重建(entry 不做 GC,只读最新一条)
|
|
3
3
|
*
|
|
4
4
|
* GoalSession 是进程内瞬态句柄(不持久化)。
|
|
5
|
-
* reconstructGoalState 从 entry 恢复状态(session_start
|
|
5
|
+
* reconstructGoalState 从 entry 恢复状态(session_start / session_tree 时调,
|
|
6
|
+
* U6c:重建输入接活跃路径裁剪——被撤子树的 goal-state 不进重建态)。
|
|
6
7
|
*
|
|
7
8
|
* FR-6.4: 删除 hasPendingInjection(僵尸字段)
|
|
8
9
|
* FR-6.7: 删除 pendingPause(ESC 改用 aborted 守卫)
|
|
@@ -10,9 +11,11 @@
|
|
|
10
11
|
* FR-3: 崩溃后保持原状态(active 重启计时;paused/blocked 保持;终态保持)
|
|
11
12
|
*/
|
|
12
13
|
|
|
14
|
+
import { filterActivePath } from "@zhushanwen/pi-session-path";
|
|
15
|
+
|
|
13
16
|
import type { GoalRuntimeState } from "./engine/types";
|
|
14
17
|
import { deserializeState, ENTRY_TYPE } from "./persistence";
|
|
15
|
-
import type { SessionEntryLike,
|
|
18
|
+
import type { SessionEntryLike, UiPort } from "./ports";
|
|
16
19
|
|
|
17
20
|
// ── 运行时句柄 ────────────────────────────────────────
|
|
18
21
|
|
|
@@ -47,10 +50,28 @@ export function createGoalSession(): GoalSession {
|
|
|
47
50
|
// 复用价值场景请参照 scheduler 的已验证方案:runtime.ts STALE_CTX_MARKER
|
|
48
51
|
// 'stale after session replacement'(子串匹配真实文案)+ 代际计数治本(G1)。
|
|
49
52
|
|
|
50
|
-
// ──
|
|
53
|
+
// ── 活跃路径裁剪(U6c)────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 活跃路径回溯所需的最小 session 视图(U6c)。getLeafId 可选:SessionPort
|
|
57
|
+
* (buildPorts 产物)不携带时按文件尾回退——session_start 时点 pi 树重放规则
|
|
58
|
+
* (叶子 = 文件最后一条 entry)使两锚等价;session_tree handler 传
|
|
59
|
+
* ctx.sessionManager(含 getLeafId),覆盖运行中树回退后 leaf ≠ 文件尾的形态。
|
|
60
|
+
*/
|
|
61
|
+
export interface ActivePathSessionView {
|
|
62
|
+
getEntries(): SessionEntryLike[];
|
|
63
|
+
getLeafId?(): string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 活跃路径裁剪(leafId 沿 parentId 回溯,撤回后被撤子树的 goal-state entry 不进
|
|
67
|
+
// 重建输入)收敛于 @zhushanwen/pi-session-path 单一实现(四包同构副本收编,防御
|
|
68
|
+
// 语义与回退口径见该包 filterActivePath 注释;runtime 侧 entry-tree-builder 保持
|
|
69
|
+
// 独立——extension 不能 import runtime 包)。
|
|
70
|
+
|
|
71
|
+
// ── reconstructGoalState(session_start / session_tree 时调)──────────
|
|
51
72
|
|
|
52
73
|
/**
|
|
53
|
-
* 从 session entries 恢复 goal state
|
|
74
|
+
* 从 session entries 恢复 goal state(U6c:重建输入 = 活跃路径投影,非全文件)。
|
|
54
75
|
*
|
|
55
76
|
* Pi SDK 的 session 是 append-only,getEntries() 返回 filter-copy——splice
|
|
56
77
|
* 无法修改真实 entries。Entry GC(goal-state 留 1、goal-history 留 20)在生产
|
|
@@ -60,11 +81,11 @@ export function createGoalSession(): GoalSession {
|
|
|
60
81
|
* paused/blocked 保持(用户/agent 主动叫停不被抹除),终态保持。
|
|
61
82
|
* FR-8.1 G-024: deserialize throw → state=null(部分损坏全丢)
|
|
62
83
|
*/
|
|
63
|
-
export function reconstructGoalState(session: GoalSession, sessionPort:
|
|
84
|
+
export function reconstructGoalState(session: GoalSession, sessionPort: ActivePathSessionView): void {
|
|
64
85
|
session.state = null;
|
|
65
|
-
const entries = sessionPort
|
|
86
|
+
const entries = filterActivePath(sessionPort);
|
|
66
87
|
|
|
67
|
-
// 找到最新的 goal-state entry
|
|
88
|
+
// 找到最新的 goal-state entry(活跃路径内从后往前)
|
|
68
89
|
let latestStateIdx = -1;
|
|
69
90
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
70
91
|
if (isGoalStateEntry(entries[i]!)) {
|