@zhushanwen/pi-goal 0.15.0 → 0.16.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 +5 -4
- package/src/__tests__/active-path.test.ts +2 -2
- package/src/__tests__/circuit-breaker.test.ts +10 -10
- package/src/__tests__/criteria-array-prompt-guidance.test.ts +2 -2
- package/src/__tests__/deserialize-state.test.ts +2 -2
- package/src/__tests__/event-adapter.test.ts +8 -8
- package/src/__tests__/goal-control-adapter.test.ts +6 -6
- package/src/__tests__/goal-control-rpc.test.ts +4 -4
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/liveness.test.ts +1 -1
- package/src/__tests__/message-end.test.ts +3 -3
- package/src/__tests__/ports.test.ts +1 -1
- package/src/__tests__/service.test.ts +2 -2
- package/src/adapters/command-adapter.ts +1 -1
- package/src/adapters/event-handlers/agent-end.ts +12 -12
- package/src/adapters/event-handlers/before-agent-start.ts +1 -1
- package/src/adapters/event-handlers/message-end.ts +3 -3
- package/src/adapters/event-handlers/session-shutdown.ts +1 -1
- package/src/adapters/event-handlers/turn-end.ts +3 -3
- package/src/adapters/goal-control-adapter.ts +11 -11
- package/src/adapters/ports.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/engine/__tests__/budget.test.ts +1 -1
- package/src/engine/__tests__/goal.test.ts +64 -10
- package/src/engine/goal.ts +82 -6
- package/src/engine/liveness.ts +2 -2
- package/src/engine/types.ts +0 -13
- package/src/index.ts +3 -3
- package/src/persistence.ts +1 -1
- package/src/ports.ts +2 -2
- package/src/projection/__tests__/widget.test.ts +3 -3
- package/src/projection/widget.ts +2 -2
- package/src/service.ts +4 -4
- package/src/session.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
30
30
|
"@earendil-works/pi-tui": "^1.0.0",
|
|
31
31
|
"typebox": "1.3.27",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.10.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhushanwen/extension-protocol": "0.
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.23.0",
|
|
36
36
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
37
|
-
"@zhushanwen/pi-session-path": "0.1.0"
|
|
37
|
+
"@zhushanwen/pi-session-path": "0.1.0",
|
|
38
|
+
"@zhushanwen/state-machine": "0.2.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"@earendil-works/pi-tui": {
|
|
@@ -127,7 +127,7 @@ interface HandlerFixture {
|
|
|
127
127
|
appendEntry: ReturnType<typeof vi.fn>;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/** 实例化 goal 工厂并捕获事件 handler(最小 pi
|
|
130
|
+
/** 实例化 goal 工厂并捕获事件 handler(最小 pi:注册点 vi.fn + 副作用侧 vi.fn) */
|
|
131
131
|
function makeHandlerFixture(): HandlerFixture {
|
|
132
132
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
|
|
133
133
|
const pi = {
|
|
@@ -145,7 +145,7 @@ function makeHandlerFixture(): HandlerFixture {
|
|
|
145
145
|
return { handlers, sendMessage: pi.sendMessage, appendEntry: pi.appendEntry };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** 最小 ctx:sessionManager(分支树)+ ui
|
|
148
|
+
/** 最小 ctx:sessionManager(分支树)+ ui 记录侧 + getContextUsage(before_agent_start 用) */
|
|
149
149
|
function makeCtx(entries: SessionEntryLike[], leafId: string | null) {
|
|
150
150
|
const setWidget = vi.fn();
|
|
151
151
|
const setStatus = vi.fn();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10
|
|
2
|
+
* 轮次活性熔断行为测试(chat-domain-v1x 设计 §3.2 D4 / W5,验收 §4 A10 的单测)
|
|
3
3
|
*
|
|
4
4
|
* 四断言(对齐 impl-plan W5 验收条款②):
|
|
5
5
|
* a. 主判据 50 次封顶必停发——含「每轮调一次工具」序列同样被封顶(正交性:
|
|
@@ -43,7 +43,7 @@ interface FakeHarness {
|
|
|
43
43
|
entries: unknown[];
|
|
44
44
|
piCalls: RecordedCall[];
|
|
45
45
|
ctxCalls: RecordedCall[];
|
|
46
|
-
/** 可控 isIdle(MF-6①
|
|
46
|
+
/** 可控 isIdle(MF-6① 检查断言用:延迟窗内有活动 turn 时置 false) */
|
|
47
47
|
isIdle: { value: boolean };
|
|
48
48
|
/**
|
|
49
49
|
* 可控 appendEntry 抛错(MF-R2-1 测试前提修正):pi.appendEntry 与 ctx 同生命周期
|
|
@@ -336,11 +336,11 @@ async function enterBackoff(): Promise<{ h: FakeHarness; session: GoalSession }>
|
|
|
336
336
|
return { h, session };
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
describe("MF-6① idle
|
|
339
|
+
describe("MF-6① idle 检查:ctx.signal 实时求值失效场景由 isIdle 拦截", () => {
|
|
340
340
|
it("延迟窗内有活动 turn(isIdle=false、signal=undefined)→ 到期不误发", async () => {
|
|
341
341
|
const { h, session } = await enterBackoff();
|
|
342
342
|
|
|
343
|
-
// agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted
|
|
343
|
+
// agent idle 后 SDK get signal 实时求值为 undefined(旧 signal?.aborted 检查恒放行);
|
|
344
344
|
// 用户在延迟窗内发新消息 → 活动 turn 占用 → isIdle=false
|
|
345
345
|
h.ctx.signal = undefined;
|
|
346
346
|
h.isIdle.value = false;
|
|
@@ -350,7 +350,7 @@ describe("MF-6① idle 守卫:ctx.signal 实时求值失效场景由 isIdle
|
|
|
350
350
|
expect(session.state!.continuationsSent).toBe(4);
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
-
it("延迟窗内 idle(isIdle=true、signal=undefined)→
|
|
353
|
+
it("延迟窗内 idle(isIdle=true、signal=undefined)→ 正常发出(检查不误伤退避路径)", async () => {
|
|
354
354
|
const { h, session } = await enterBackoff();
|
|
355
355
|
|
|
356
356
|
h.ctx.signal = undefined;
|
|
@@ -403,7 +403,7 @@ describe("MF-6② stale 保护:timer 回调内异常不冒泡(catch 块自
|
|
|
403
403
|
});
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
describe("MF-R2-1
|
|
406
|
+
describe("MF-R2-1 从根源修复:session_shutdown 取消旧退避 timer", () => {
|
|
407
407
|
it("session_shutdown(invalidate 前触发)后旧 timer 到期不发;全量 stale 下不崩、不冒泡", async () => {
|
|
408
408
|
const { h, session } = await enterBackoff();
|
|
409
409
|
|
|
@@ -430,7 +430,7 @@ describe("MF-R2-1 根修:session_shutdown 取消旧退避 timer", () => {
|
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
-
describe("MF-6③ timer
|
|
433
|
+
describe("MF-6③ timer 清理侧:session_start / before_agent_start 取消旧 timer", () => {
|
|
434
434
|
it("session_start 重建 → 旧退避 timer 被取消,到期不发", async () => {
|
|
435
435
|
const { h, session } = await enterBackoff();
|
|
436
436
|
|
|
@@ -459,13 +459,13 @@ describe("MF-6③ timer 清理面:session_start / before_agent_start 取消旧
|
|
|
459
459
|
// 端口改造前 backoff timer 到期伪装用户消息投递;改造后
|
|
460
460
|
// deliverContinuation 经端口发 custom message 且 options 含 triggerTurn: true——
|
|
461
461
|
// 非 streaming idle 场景真实开轮(改造前 append 不开轮 = 死消息)。
|
|
462
|
-
// isIdle=false 拦截见 MF-6
|
|
462
|
+
// isIdle=false 拦截见 MF-6①;封顶记账检查见「主判据」describe;此处锁定到期
|
|
463
463
|
// 发出路径的 options 契约与记账恰好 +1。
|
|
464
464
|
describe("A8:退避 timer 到期 → triggerTurn: true 开轮发出", () => {
|
|
465
|
-
it("isIdle
|
|
465
|
+
it("isIdle 检查通过后发出:custom message 四要素齐备,记账恰好 +1", async () => {
|
|
466
466
|
const { h, session } = await enterBackoff();
|
|
467
467
|
|
|
468
|
-
vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true →
|
|
468
|
+
vi.advanceTimersByTime(20_000); // 2×base 到期,isIdle 默认 true → 检查放行
|
|
469
469
|
|
|
470
470
|
const sends = followUpSends(h);
|
|
471
471
|
expect(sends).toHaveLength(5); // 4 立即 + 1 到期发出
|
|
@@ -48,12 +48,12 @@ const successCriteriaSchema = GoalControlParams.properties.successCriteria;
|
|
|
48
48
|
const PARAM_DESCRIPTION =
|
|
49
49
|
typeof successCriteriaSchema.description === "string" ? successCriteriaSchema.description : "";
|
|
50
50
|
|
|
51
|
-
// ── 第 2 层切片:工具 description 模板字面量("description: `"
|
|
51
|
+
// ── 第 2 层切片:工具 description 模板字面量("description: `" 到结束反引号)──
|
|
52
52
|
|
|
53
53
|
const TOOL_DESCRIPTION = sliceBetween(ADAPTER_SRC, "description: `", "`");
|
|
54
54
|
|
|
55
55
|
// ── 第 3 层切片:promptGuidelines 赋值块 ─────────────────────
|
|
56
|
-
//
|
|
56
|
+
// 结束 "]" 不能用 indexOf 找——块内 guideline 文本自身含 "string[]" 的 "]",
|
|
57
57
|
// 故按行扫描 trim() === "]," 的行作块尾(缩进无关)。
|
|
58
58
|
|
|
59
59
|
function extractPromptGuidelinesBlock(src: string): string {
|
|
@@ -181,9 +181,9 @@ describe("deserializeState — 旧数据迁移(time_limited → budget_limited
|
|
|
181
181
|
expect((state as unknown as { timeWarning90Sent?: unknown }).timeWarning90Sent).toBeUndefined();
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it("归一化后是合法终态:/goal clear
|
|
184
|
+
it("归一化后是合法终态:/goal clear 检查(!isTerminalStatus)跳过 transitionStatus,不 throw", () => {
|
|
185
185
|
const state = deserializeState(LEGACY_DATA);
|
|
186
|
-
// handleClear(command-adapter.ts
|
|
186
|
+
// handleClear(command-adapter.ts)的检查:!isTerminalStatus 为 false → 直接 clearSession,
|
|
187
187
|
// 不再走 finalizeAndPersist("cancelled") 的 transitionStatus 查表(旧值会 throw)。
|
|
188
188
|
expect(isTerminalStatus(state.status)).toBe(true);
|
|
189
189
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* event-handlers 行为测试 — agent_end / before_agent_start 核心逻辑
|
|
3
3
|
*
|
|
4
|
-
* 覆盖 agent_end 的 continuation + ESC
|
|
4
|
+
* 覆盖 agent_end 的 continuation + ESC 检查(FR-6.7)+
|
|
5
5
|
* before_agent_start 的 AUTO_CLEAR/context wrap-up。
|
|
6
6
|
*
|
|
7
7
|
* 全解耦后不再测 allTasksDone followUp(原依赖 pi.__todoGetList,已删)。
|
|
@@ -100,9 +100,9 @@ function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeSta
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// ── handleAgentEnd:ESC
|
|
103
|
+
// ── handleAgentEnd:ESC 检查 ──────────────────────────
|
|
104
104
|
|
|
105
|
-
describe("handleAgentEnd — FR-6.7 ESC
|
|
105
|
+
describe("handleAgentEnd — FR-6.7 ESC 检查", () => {
|
|
106
106
|
it("aborted=true:不发 continuation、不做 budget 检查、goal 保持 active", async () => {
|
|
107
107
|
const { pi, calls: piCalls, states } = makeFakePi();
|
|
108
108
|
const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
|
|
@@ -174,9 +174,9 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
174
174
|
expect(sendContext[0]!.content).toContain("[GOAL]");
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// ── pending
|
|
177
|
+
// ── pending 检查:background subagent/workflow 运行时不死循环 ──
|
|
178
178
|
|
|
179
|
-
it("有活跃 pending → 不发 continuation(pending
|
|
179
|
+
it("有活跃 pending → 不发 continuation(pending 检查生效)", async () => {
|
|
180
180
|
const { pi, calls: piCalls } = makeFakePi();
|
|
181
181
|
const { ctx, calls: ctxCalls } = makeFakeCtx({
|
|
182
182
|
entries: [
|
|
@@ -258,7 +258,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
258
258
|
expect(data?.ids).toEqual(["bg-1", "bg-2"]); // ids 透出
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
-
it("重复 register 同 id → 去重计为 1(seen.has
|
|
261
|
+
it("重复 register 同 id → 去重计为 1(seen.has 检查)", async () => {
|
|
262
262
|
const { pi, calls: piCalls, states } = makeFakePi();
|
|
263
263
|
const { ctx, calls: ctxCalls } = makeFakeCtx({
|
|
264
264
|
entries: [
|
|
@@ -272,7 +272,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
272
272
|
await handleAgentEnd(pi, session, ctx);
|
|
273
273
|
const all = allCalls(piCalls, ctxCalls);
|
|
274
274
|
|
|
275
|
-
//
|
|
275
|
+
// 检查触发:count>0 不发 continuation
|
|
276
276
|
const followUp = all.filter(
|
|
277
277
|
(c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
|
|
278
278
|
);
|
|
@@ -298,7 +298,7 @@ describe("handleAgentEnd — continuation", () => {
|
|
|
298
298
|
await handleAgentEnd(pi, session, ctx);
|
|
299
299
|
const all = allCalls(piCalls, ctxCalls);
|
|
300
300
|
|
|
301
|
-
// count=0
|
|
301
|
+
// count=0,检查不触发,走正常 continuation
|
|
302
302
|
const followUp = all.filter(
|
|
303
303
|
(c) => c.kind === "sendContext" && (c.payload as { deliverAs?: string } | undefined)?.deliverAs === "followUp",
|
|
304
304
|
);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* goal_control adapter 测试 — handler 用 fake ports
|
|
3
3
|
*
|
|
4
4
|
* 覆盖 #3 验收:
|
|
5
|
-
* - complete: active
|
|
6
|
-
* - report_blocked: active
|
|
5
|
+
* - complete: active 检查 + evidence 必填 + finalizeAndPersist(status→complete + history)
|
|
6
|
+
* - report_blocked: active 检查 + reason 必填 + tickState + transitionStatus(active→blocked) + persistState
|
|
7
7
|
*
|
|
8
8
|
* 全解耦后不再测 todo 前置检查(checkCompletePrerequisites / buildProgressInput / findIncompleteTodos 已删)。
|
|
9
9
|
* complete 不做 todo 完成硬检查——AI 自行判断(prompt 软建议)。
|
|
@@ -74,7 +74,7 @@ const activeState = (overrides: Partial<GoalRuntimeState> = {}): GoalRuntimeStat
|
|
|
74
74
|
|
|
75
75
|
// ── handleComplete ───────────────────────────────────
|
|
76
76
|
|
|
77
|
-
describe("handleComplete — active
|
|
77
|
+
describe("handleComplete — active 检查 + evidence + finalizeAndPersist", () => {
|
|
78
78
|
it("active + evidence → status complete + 写 history", () => {
|
|
79
79
|
const session = createGoalSession();
|
|
80
80
|
session.state = activeState();
|
|
@@ -117,7 +117,7 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
|
|
|
117
117
|
|
|
118
118
|
// ── handleCreate ─────────────────────────────────────
|
|
119
119
|
|
|
120
|
-
describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional +
|
|
120
|
+
describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态检查 + createGoal", () => {
|
|
121
121
|
it("无旧 goal + slug + objective → status active + slug 写入 + 持久化 + notify", () => {
|
|
122
122
|
const session = createGoalSession();
|
|
123
123
|
const ports = makeFakePorts();
|
|
@@ -175,7 +175,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
const NON_TERMINAL_STATUSES: GoalRuntimeState["status"][] = ["active", "paused", "blocked"];
|
|
178
|
-
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25
|
|
178
|
+
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态检查)", (status) => {
|
|
179
179
|
const session = createGoalSession();
|
|
180
180
|
session.state = activeState({ status });
|
|
181
181
|
expect(() =>
|
|
@@ -232,7 +232,7 @@ describe("handleCreate — objective/successCriteria 必填(handler 运行时
|
|
|
232
232
|
|
|
233
233
|
// ── handleReportBlocked ──────────────────────────────
|
|
234
234
|
|
|
235
|
-
describe("handleReportBlocked — active
|
|
235
|
+
describe("handleReportBlocked — active 检查 + tick + transition + persist", () => {
|
|
236
236
|
it("active + reason → status blocked + reason 记录 + tick 累加", () => {
|
|
237
237
|
const session = createGoalSession();
|
|
238
238
|
const past = Date.now() - 4000;
|
|
@@ -17,7 +17,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
17
17
|
import { registerGoalControlTool, type GoalControlDetails } from "../adapters/goal-control-adapter";
|
|
18
18
|
import { createGoalSession } from "../session";
|
|
19
19
|
|
|
20
|
-
// ── mock:logger + updateWidget(UI
|
|
20
|
+
// ── mock:logger + updateWidget(UI 通道降级用例断言范围;其余用例下两者为 no-op,
|
|
21
21
|
// 与真实路径行为等价——fixture 的 setWidget/notify 本就是 no-op)──
|
|
22
22
|
const loggerWarnSpy = vi.hoisted(() => vi.fn());
|
|
23
23
|
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
@@ -150,9 +150,9 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it("RPC + session.state = null → 前置 handler throw(分支不可达)", async () => {
|
|
153
|
-
// execute 路径上,create 总会 set session.state;complete/report_blocked
|
|
153
|
+
// execute 路径上,create 总会 set session.state;complete/report_blocked 前置检查
|
|
154
154
|
// 要求 session.state 非 null 否则 throw——所以 session.state=null 时不会走到返回路径。
|
|
155
|
-
// 验证:全新 session(state=null)下,complete
|
|
155
|
+
// 验证:全新 session(state=null)下,complete 前置检查先 throw。
|
|
156
156
|
const { pi, ctx } = makeFixture("rpc");
|
|
157
157
|
let captured: CapturedTool | undefined;
|
|
158
158
|
const capturePi = {
|
|
@@ -163,7 +163,7 @@ describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
|
|
|
163
163
|
} as unknown as ExtensionAPI;
|
|
164
164
|
registerGoalControlTool(capturePi, createGoalSession());
|
|
165
165
|
const tool2 = captured!;
|
|
166
|
-
// complete 在 state=null 时 throw(handleComplete
|
|
166
|
+
// complete 在 state=null 时 throw(handleComplete 前置检查)
|
|
167
167
|
await expect(
|
|
168
168
|
tool2.execute(
|
|
169
169
|
"id",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* index.ts 测试 — 工厂入口 + 跨扩展 API(goalInit slot)
|
|
3
3
|
*
|
|
4
4
|
* 覆盖 T1.8 (NFR-AC-8):goalInit 签名 (objective, budget, ctx, slug?, successCriteria?),
|
|
5
|
-
* 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active
|
|
5
|
+
* 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 检查由 service.test.ts 覆盖。
|
|
6
6
|
*
|
|
7
7
|
* 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再从 globalThis slot 读取 goalInit——
|
|
8
8
|
* mock 形态与真实通道同构(goal-bridge-cross-extension.md §3.4:桥的元教训是 mock 世界
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* engine/liveness.ts 纯函数测试 — 轮次活性熔断判定单元(chat-domain-v1x D4 / W5)
|
|
3
3
|
*
|
|
4
4
|
* 覆盖:env 配置解析(合法/非法/缺省)、退避等级数学(第 N 轮起 ×2、指数天花板)、
|
|
5
|
-
* 进展信号边界、toolCall 块计数(duck-typed
|
|
5
|
+
* 进展信号边界、toolCall 块计数(duck-typed 检查)、pending 集合变化判定。
|
|
6
6
|
* 行为级四断言(封顶/退避/恢复/defer 去重经 handleAgentEnd)见 circuit-breaker.test.ts。
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* message_end handler 测试(D13① role 过滤 + FR-6.7 ESC
|
|
2
|
+
* message_end handler 测试(D13① role 过滤 + FR-6.7 ESC 检查)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 断言范围:仅 user/assistant 的 message_end 进入 service 消费;system(pi 1.0
|
|
5
5
|
* 新形态)/ toolResult / 缺 role 在 handler 层显式拒绝;assistant 正常透传。
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -73,7 +73,7 @@ describe("handleMessageEnd(D13① role 过滤)", () => {
|
|
|
73
73
|
expect(spy).not.toHaveBeenCalled();
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it("ESC
|
|
76
|
+
it("ESC 检查先于 role 过滤(aborted 直接返回)", async () => {
|
|
77
77
|
const session = makeSession();
|
|
78
78
|
const spy = vi.mocked(applyEvent);
|
|
79
79
|
await handleMessageEnd(session, ctx(true), messageEndEvent("assistant"));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - tui 模式:text 臂透传(pi 原生文本行,无 marker)
|
|
7
7
|
* - undefined:清除语义(模式无关)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 模式分派检查单点在 protocol helpers.ts(检查单点化),本测试只验证委托接线。
|
|
10
10
|
*
|
|
11
11
|
* 运行:cd extensions/universal/goal && npx vitest run src/__tests__/ports.test.ts
|
|
12
12
|
*/
|
|
@@ -123,7 +123,7 @@ describe("finalizeGoal — history 写入矩阵", () => {
|
|
|
123
123
|
const ports = makeFakePorts();
|
|
124
124
|
const state = makeState();
|
|
125
125
|
state.status = "complete";
|
|
126
|
-
// transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus
|
|
126
|
+
// transitionStatus 查表:终态不可转,调用方须先 isTerminalStatus 检查
|
|
127
127
|
expect(() => finalizeGoal(state, "cancelled", ports)).toThrow();
|
|
128
128
|
expect(state.status).toBe("complete");
|
|
129
129
|
});
|
|
@@ -175,7 +175,7 @@ describe("applyEvent — 简单事件", () => {
|
|
|
175
175
|
usage: { input: 100, output: 50, cacheRead: 20 },
|
|
176
176
|
},
|
|
177
177
|
});
|
|
178
|
-
// 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus
|
|
178
|
+
// 回归修复:blocked 状态不累加 token(原 bug:缺 isActiveStatus 检查)
|
|
179
179
|
expect(session.state.tokensUsed).toBe(before);
|
|
180
180
|
});
|
|
181
181
|
|
|
@@ -353,7 +353,7 @@ function handleUpdate(
|
|
|
353
353
|
* 这样 goal 创建的唯一路径是 goal_control toolcall(统一入口),
|
|
354
354
|
* slug/objective/budget 都由 AI 在 toolcall 时决定。
|
|
355
355
|
*
|
|
356
|
-
* D25
|
|
356
|
+
* D25 检查仍在此处预检:非终态旧 goal(active/paused/blocked)→ 拒绝,
|
|
357
357
|
* 提示 /goal resume 或 /goal clear(避免 AI 在已有未完成 goal 时重复创建)。
|
|
358
358
|
*
|
|
359
359
|
* budget flag(--tokens)写入消息体,让 AI 原样传给 create。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 事件 6: agent_end(FR-8.7 + ESC
|
|
2
|
+
* 事件 6: agent_end(FR-8.7 + ESC 检查 + 并发保护)。
|
|
3
3
|
*
|
|
4
4
|
* 关键约定:
|
|
5
5
|
* - FR-8.2 G-021 防重入:session.isProcessing 入口加锁,finally 释放
|
|
6
6
|
* - FR-8.2 G-020 stale 快照:入口 makeStaleChecker snapshot goalId,每个副作用前 checkStale
|
|
7
|
-
* - FR-6.7 ESC
|
|
7
|
+
* - FR-6.7 ESC 检查(最关键):ctx.signal?.aborted → 不发 continuation、不做 budget 检查、
|
|
8
8
|
* 不做任何状态变更,goal 保持 active,等用户下次输入恢复
|
|
9
9
|
*
|
|
10
10
|
* #8 后流程:budget 预警/steering → continuation 去抖 + 轮次活性熔断(W5:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 全解耦:不再做 allTasksDone followUp(原依赖 pi.__todoGetList,跨 ext 失效)。
|
|
16
16
|
* todo 是否全完成由 AI 自行判断(prompt 软建议)。
|
|
17
17
|
*
|
|
18
|
-
* ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC
|
|
18
|
+
* ESC 路径:aborted 时直接 return(goal 保持 active)。注意 ESC 检查在终态/非 active
|
|
19
19
|
* 检查之后——终态 goal 仍走终态 notify(不被 ESC 影响),非 active 状态直接返回。
|
|
20
20
|
*/
|
|
21
21
|
|
|
@@ -63,7 +63,7 @@ export async function handleAgentEnd(
|
|
|
63
63
|
}
|
|
64
64
|
if (!isActiveStatus(session.state.status)) return;
|
|
65
65
|
|
|
66
|
-
// FR-6.7 ESC
|
|
66
|
+
// FR-6.7 ESC 检查(最关键):aborted 时 goal 保持 active,不做任何副作用
|
|
67
67
|
if (ctx.signal?.aborted) {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
@@ -152,7 +152,7 @@ async function handleBudgetChecks(
|
|
|
152
152
|
* - W5 主判据:continuation 连发总次数封顶(默认 50,PI_GOAL_CONTINUATION_CAP)——
|
|
153
153
|
* 封顶必停发 + notify 用户 + goal 保持 active(不擅自终态)。与「是否调工具」
|
|
154
154
|
* 正交:计数只在 deliverContinuation 实发时 +1,绝不被工具调用清零
|
|
155
|
-
* (2026-09-08 事故:pending
|
|
155
|
+
* (2026-09-08 事故:pending 检查击穿后 64 分钟 ~320 turn 空转,无任何熔断)。
|
|
156
156
|
* - W5 辅判据:连续 N 轮(默认 5)无工具调用且 tokenDelta 低于阈值 → continuation
|
|
157
157
|
* 延迟 base×2^level 发出(间隔 ×2 递增);真实进展清零退避计数(不动总次数)。
|
|
158
158
|
* - W5 defer 通知去重:pending 集合不变的连续 defer 轮不重复 notify。
|
|
@@ -188,7 +188,7 @@ async function handleContinuation(
|
|
|
188
188
|
isProgressSignal(toolDelta, tokenDelta, cfg),
|
|
189
189
|
);
|
|
190
190
|
|
|
191
|
-
// pending
|
|
191
|
+
// pending 检查:有活跃 background subagent/workflow 时不发 continuation。
|
|
192
192
|
// 靠 subagent/workflow 完成时 sendMessage({triggerTurn:true},缺省 deliverAs 即 steer) 自然唤醒主 agent;
|
|
193
193
|
// goal 抢先催会与异步任务完成通知叠加,形成“停不下来”的死循环——continuation 排入
|
|
194
194
|
// _followUpMessages 队列 → _handlePostAgentRun 的 hasQueuedMessages() 返回 true → agent.continue()
|
|
@@ -196,8 +196,8 @@ async function handleContinuation(
|
|
|
196
196
|
// 刻意不校验 expiresAt:长任务 subagent(>1h TTL)完成时仍 triggerTurn 唤醒主 agent,
|
|
197
197
|
// 按 TTL 判非活跃会让死循环在长任务场景复现。
|
|
198
198
|
// [W4 读侧过滤①消费口] 传当前 session 基准:fork 继承的父级注册残留不进差集
|
|
199
|
-
//
|
|
200
|
-
// registry-fork-filter.test.ts 证不传则虚增,A9②
|
|
199
|
+
// (检查不幻 defer);设计 D4「读侧过滤一刀」在本消费口的落地(W6 探针实测
|
|
200
|
+
// registry-fork-filter.test.ts 证不传则虚增,A9② 验收标准)。
|
|
201
201
|
const pendingOps = countActiveFromEntries(entries, {
|
|
202
202
|
currentSessionId: ctx.sessionManager.getSessionId(),
|
|
203
203
|
});
|
|
@@ -239,7 +239,7 @@ async function handleContinuation(
|
|
|
239
239
|
// 空 turn:已 persist,不发 continuation
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
|
-
//
|
|
242
|
+
// 终态检查:persistAndUpdate 可能把 goal 转为 budget_limited 终态。
|
|
243
243
|
// 此时不应发 continuation(deliverAs:"followUp" 会触发新 turn,让已耗尽预算的 agent 再跑一轮)。
|
|
244
244
|
if (isTerminalStatus(state.status)) return;
|
|
245
245
|
|
|
@@ -319,11 +319,11 @@ async function handleContinuation(
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
* 退避 timer
|
|
322
|
+
* 退避 timer 到期后的发射前检查 + 实发(延迟期间状态可能已变):
|
|
323
323
|
* - goal 未被覆盖/清除(goalId 比对)、仍 active
|
|
324
|
-
* - MF-6① idle
|
|
324
|
+
* - MF-6① idle 检查:ctx.signal 为实时求值(pi-agent-core agent.js get signal 返回
|
|
325
325
|
* activeRun?.abortController.signal),agent idle 后恒 undefined,`signal?.aborted`
|
|
326
|
-
*
|
|
326
|
+
* 恒放行、不可作检查;改用 ctx.isIdle()(SDK types.d.ts L232)——延迟窗内有活动
|
|
327
327
|
* turn 时不发,该轮 agent_end 会按最新状态重新决策
|
|
328
328
|
* - 未封顶、无活跃 pending(等待期新出现的后台任务由其完成通知唤醒,goal 不抢先催)
|
|
329
329
|
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 2. ADR-002:Context 使用率 > 85% → 保持 active + 注入 wrap-up 指令
|
|
9
9
|
* 3. 正常:注入 contextInjectionPrompt
|
|
10
10
|
*
|
|
11
|
-
* 无 ESC
|
|
11
|
+
* 无 ESC 检查(before_agent_start 是 agent 开始前的信号,此时无 aborted 可能)。
|
|
12
12
|
*
|
|
13
13
|
* 全解耦:不再做 staleness 检测(原依赖 pi.__todoGetList,跨 ext 失效),
|
|
14
14
|
* 不再探测 plan extension(原 typeof pi.__planStart,跨 ext 失效)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 事件 3: message_end(FR-6.7 ESC
|
|
2
|
+
* 事件 3: message_end(FR-6.7 ESC 检查 + token 累加)。
|
|
3
3
|
*
|
|
4
|
-
* FR-6.7 ESC
|
|
4
|
+
* FR-6.7 ESC 检查:ctx.signal.aborted 时跳过 token 累加。
|
|
5
5
|
* FR-8.6: token 累加算法(委托 service.applyEvent("message_end"))。
|
|
6
6
|
*
|
|
7
7
|
* role 过滤(D13①,rename-session 先例同形态):仅 user/assistant 的 message_end
|
|
@@ -37,7 +37,7 @@ export async function handleMessageEnd(
|
|
|
37
37
|
event: MessageEndEvent,
|
|
38
38
|
): Promise<void> {
|
|
39
39
|
if (!session.state) return;
|
|
40
|
-
// FR-6.7 ESC
|
|
40
|
+
// FR-6.7 ESC 检查
|
|
41
41
|
if (ctx.signal?.aborted) return;
|
|
42
42
|
|
|
43
43
|
// D13① role 过滤:仅对话轴两形态(AgentMessage role 域为开放联合,宽比较)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 事件 7: session_shutdown(MF-R2-1
|
|
2
|
+
* 事件 7: session_shutdown(MF-R2-1 从根源修复:invalidate 前取消旧退避 timer)。
|
|
3
3
|
*
|
|
4
4
|
* SDK 触发序(@earendil-works/pi-coding-agent 0.84.4 实装核对)——所有使旧
|
|
5
5
|
* runner 失效的路径都先 emit session_shutdown 再 invalidate,此刻 pi/ctx 仍可用:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 事件 2: turn_end(FR-6.7 ESC
|
|
2
|
+
* 事件 2: turn_end(FR-6.7 ESC 检查 + 递增)。
|
|
3
3
|
*
|
|
4
|
-
* FR-6.7 ESC
|
|
4
|
+
* FR-6.7 ESC 检查:ctx.signal.aborted 时跳过递增(ESC 不算 goal turn)。
|
|
5
5
|
* 正常路径:currentTurnIndex++ + updateWidget。
|
|
6
6
|
*
|
|
7
7
|
* 委托 service.applyEvent("turn_end") 递增 currentTurnIndex(H3:applyEvent 改 void,
|
|
@@ -23,7 +23,7 @@ export async function handleTurnEnd(
|
|
|
23
23
|
ctx: ExtensionContext,
|
|
24
24
|
): Promise<void> {
|
|
25
25
|
if (!session.state) return;
|
|
26
|
-
// FR-6.7 ESC
|
|
26
|
+
// FR-6.7 ESC 检查
|
|
27
27
|
if (ctx.signal?.aborted) return;
|
|
28
28
|
|
|
29
29
|
const ports = buildPorts(pi, ctx);
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* #3:替代已删除的 goal_manager tool。
|
|
5
5
|
*
|
|
6
6
|
* 职责分层:
|
|
7
|
-
* - execute 层(adapter 职责):signal
|
|
7
|
+
* - execute 层(adapter 职责):signal 检查
|
|
8
8
|
* - handler 层(goal 业务,契约对齐 code-architecture §3 handleCreate/handleComplete/handleReportBlocked):
|
|
9
|
-
* active
|
|
9
|
+
* active 检查 + evidence/reason 空串校验 + 状态转换
|
|
10
10
|
*
|
|
11
11
|
* 全解耦:goal 不再读 todo/plan 状态。complete 不做 todo 完成前置硬检查——
|
|
12
12
|
* todo 是否全完成由 AI 自行判断,goal 仅通过 prompt 软建议(见 prompts.ts)。
|
|
@@ -176,7 +176,7 @@ export function buildGoalControlSummary(
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* create 业务逻辑:objective + successCriteria 必填(schema 层已强制,此处仅校验空串)
|
|
179
|
-
* + 非终态旧 goal
|
|
179
|
+
* + 非终态旧 goal 检查 + service.createGoal。
|
|
180
180
|
*
|
|
181
181
|
* slug:AI 生成的短标识,仅 widget 标题 + history 用,不注入 prompt。真 optional。
|
|
182
182
|
* objective:完整描述,注入每轮 context prompt(保证方向感)。
|
|
@@ -184,9 +184,9 @@ export function buildGoalControlSummary(
|
|
|
184
184
|
* 全解耦:不读 todo/plan。toolcall 时 AI 已在 turn 中,**不**发消息注入
|
|
185
185
|
* (AI 返回后自行续跑,对齐 Codex create_goal)。
|
|
186
186
|
*
|
|
187
|
-
*
|
|
187
|
+
* 检查用 D25 严格语义:非终态 active/paused/blocked 全挡,提示用 /goal resume 或
|
|
188
188
|
* /goal clear——防 AI 静默覆盖含未完成工作的 goal。
|
|
189
|
-
* 终态旧 goal 走 createGoal 快速路径覆盖(createGoal 内部 active
|
|
189
|
+
* 终态旧 goal 走 createGoal 快速路径覆盖(createGoal 内部 active 检查,终态可覆盖)。
|
|
190
190
|
*
|
|
191
191
|
* 参数校验拆分至 requireCreateObjective / requireCreateSuccessCriteria /
|
|
192
192
|
* resolveCreateBudget(圈复杂度拆分),校验顺序与错误消息不变。
|
|
@@ -247,7 +247,7 @@ export function handleCreate(
|
|
|
247
247
|
const slugInput = params.slug?.trim();
|
|
248
248
|
const successCriteria = requireCreateSuccessCriteria(params);
|
|
249
249
|
|
|
250
|
-
// D25
|
|
250
|
+
// D25 严格检查:非终态旧 goal(active/paused/blocked)→ 拒绝创建(防静默覆盖未完成工作)
|
|
251
251
|
if (session.state && !isTerminalStatus(session.state.status)) {
|
|
252
252
|
throw new Error(
|
|
253
253
|
`Goal already active (status: ${session.state.status}). Use /goal resume to continue or /goal clear to reset before creating a new one.`,
|
|
@@ -260,7 +260,7 @@ export function handleCreate(
|
|
|
260
260
|
// FR-3.1: 唯一创建入口。终态旧 goal 走覆盖快速路径。
|
|
261
261
|
const created = createGoal(session, objective, budget, ports, slugInput, successCriteria);
|
|
262
262
|
if (!created) {
|
|
263
|
-
// createGoal 内部 active
|
|
263
|
+
// createGoal 内部 active 检查兜底(理论上上面检查已挡;防御性)
|
|
264
264
|
throw new Error("Goal already active. Cannot create a new one.");
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -278,7 +278,7 @@ export function handleCreate(
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
|
-
* complete 业务逻辑:active
|
|
281
|
+
* complete 业务逻辑:active 检查 + evidence 空串校验 + finalizeAndPersist。
|
|
282
282
|
*
|
|
283
283
|
* 全解耦后不再做 todo 完成前置检查——todo 是否全完成由 AI 自行判断(prompt 软建议)。
|
|
284
284
|
*/
|
|
@@ -313,7 +313,7 @@ export function handleComplete(
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
|
-
* report_blocked 业务逻辑:active
|
|
316
|
+
* report_blocked 业务逻辑:active 检查 + reason 空串校验 + tickState + transitionStatus + persistState。
|
|
317
317
|
*
|
|
318
318
|
* 必须在 transitionStatus **之前** tickState,使 tick 看到 active 状态并累加当前运行段;
|
|
319
319
|
* 否则转 blocked 后 persistState 内部的 tick 因 status≠active 不累加,丢失最后一段运行时间。
|
|
@@ -352,7 +352,7 @@ export function handleReportBlocked(
|
|
|
352
352
|
return { action: "report_blocked", goalId: state.goalId, status: state.status };
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
/** renderResult 的 result 是否含 details
|
|
355
|
+
/** renderResult 的 result 是否含 details 字段(类型检查,替代全可选结构断言 as {details?})。
|
|
356
356
|
* 收紧:除检查 "details" in r 外,还验证其值为 object 或 undefined(防 details 是 string/number
|
|
357
357
|
* 时下游读 d.status 得到 undefined 却被类型系统当作 GoalControlDetails)。 */
|
|
358
358
|
function hasGoalDetails(r: unknown): r is { details?: GoalControlDetails } {
|
|
@@ -435,7 +435,7 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
435
435
|
|
|
436
436
|
renderCall(args: Record<string, unknown>, theme: Theme): Text {
|
|
437
437
|
// TUI 渲染先于 schema 校验(args 是未经 schema 收窄的原始形态):非对象整体
|
|
438
|
-
// 走安全占位(不抛错);action/slug 逐个 typeof
|
|
438
|
+
// 走安全占位(不抛错);action/slug 逐个 typeof 检查替代裸断言——非法 action
|
|
439
439
|
// 落 report_blocked 回落分支(与原行为一致)
|
|
440
440
|
if (typeof args !== "object" || args === null) {
|
|
441
441
|
return new Text(
|
package/src/adapters/ports.ts
CHANGED
|
@@ -42,8 +42,8 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
|
|
|
42
42
|
const uiPort: UiPort = {
|
|
43
43
|
setWidget(name, content): void {
|
|
44
44
|
// 双模 payload 委托 protocol setWidgetDual:推送/清屏 × GUI/TUI 的模式分派
|
|
45
|
-
// 由 helper
|
|
46
|
-
// 本 adapter 不再自持 isGui
|
|
45
|
+
// 由 helper 单点内化(检查单点化说明见 extension-protocol helpers.ts),
|
|
46
|
+
// 本 adapter 不再自持 isGui 判定/检查注释。
|
|
47
47
|
//
|
|
48
48
|
// 断言根因:pi ExtensionUIContext.custom 是泛型方法(返回 Promise<T>),
|
|
49
49
|
// 与 GuiContext.ui.custom 的具体返回类型静态不兼容,但 mode/hasUI/ui 其余
|
package/src/constants.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const MAX_HISTORY_ENTRIES = 20; // goal-history entry GC 上限
|
|
|
38
38
|
|
|
39
39
|
// ── 轮次活性熔断(chat-domain-v1x D4 / W5)─────────────
|
|
40
40
|
//
|
|
41
|
-
// 事故背景(2026-09-08,64 分钟 ~320 turn 空转烧 2.93M token):pending
|
|
41
|
+
// 事故背景(2026-09-08,64 分钟 ~320 turn 空转烧 2.93M token):pending 检查被
|
|
42
42
|
// 「等待已注销的后台任务」击穿后,goal 每轮注入 continuation、主 agent 每轮回一句
|
|
43
43
|
// 等待,无任何无进展熔断。双维度封顶:主判据总量上限(与是否调工具正交,不可被
|
|
44
44
|
// 任何目标行为绕过)+ 辅判据无进展退避(间隔 ×2 递增)。全部可经 PI_GOAL_* env 覆盖。
|
|
@@ -125,7 +125,7 @@ describe("getBudgetSeverity", () => {
|
|
|
125
125
|
const [r1, r2] = a <= b ? [a, b] : [b, a];
|
|
126
126
|
return RANK[getBudgetSeverity(r1)] <= RANK[getBudgetSeverity(r2)];
|
|
127
127
|
});
|
|
128
|
-
// 三分区 property
|
|
128
|
+
// 三分区 property 固定阈值边界(捕捉 0.9/0.7 被改)
|
|
129
129
|
it.prop([f64(0.9, 1)])("r ≥ 0.9 → danger", (r) => getBudgetSeverity(r) === "danger");
|
|
130
130
|
it.prop([f64(0.7, 0.9).filter((r) => r < 0.9)])("0.7 ≤ r < 0.9 → warn", (r) => getBudgetSeverity(r) === "warn");
|
|
131
131
|
it.prop([f64(0, 0.7).filter((r) => r < 0.7)])("r < 0.7 → ok", (r) => getBudgetSeverity(r) === "ok");
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/goal.ts 测试 — Goal 状态机 + createGoalState
|
|
3
3
|
*
|
|
4
|
-
* transitionStatus
|
|
5
|
-
*
|
|
4
|
+
* transitionStatus 完备性两层:fast-check 抽样(不 throw ⟺ (from,to) ∈ 期望合法边表)
|
|
5
|
+
* + (from,target) 6×6 穷举基线(M3 迁移对账矩阵,36 组合全跑)。期望边表硬编码自
|
|
6
|
+
* 领域语义、独立于实现(不 import 实现侧转移表——M3 迁移前后同一期望表对账)。
|
|
7
|
+
* 机器表级另设 generateTableTests 穷举生成组(D5/D6 每步判据第二款)——直测
|
|
8
|
+
* GOAL_MACHINE 全状态×全事件,与包装层基线互为独立防线。
|
|
6
9
|
*/
|
|
7
10
|
import { describe, expect } from "vitest";
|
|
8
11
|
import { it, fc } from "@fast-check/vitest";
|
|
@@ -10,8 +13,8 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
10
13
|
import { join } from "node:path";
|
|
11
14
|
import { fileURLToPath } from "node:url";
|
|
12
15
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { generateTableTests } from "@zhushanwen/state-machine/testing";
|
|
17
|
+
import { GOAL_MACHINE, createGoalState, isActiveStatus, isTerminalStatus, transitionStatus } from "../goal";
|
|
15
18
|
import type { GoalStatus } from "../types";
|
|
16
19
|
|
|
17
20
|
const STATUS_VALUES: GoalStatus[] = ["active", "paused", "blocked", "complete", "budget_limited", "cancelled"];
|
|
@@ -20,6 +23,21 @@ const statusArb = fc.constantFrom(...STATUS_VALUES);
|
|
|
20
23
|
const TERMINAL: ReadonlySet<GoalStatus> = new Set(["complete", "budget_limited", "cancelled"]);
|
|
21
24
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
25
|
|
|
26
|
+
// 期望合法边表(M3 迁移基线 oracle):9 条合法边硬编码自领域语义——
|
|
27
|
+
// 用户 pause/resume/cancel、agent report_blocked/complete、token 预算耗尽。
|
|
28
|
+
const EXPECTED_LEGAL_EDGES: ReadonlyArray<readonly [GoalStatus, GoalStatus]> = [
|
|
29
|
+
["active", "paused"],
|
|
30
|
+
["active", "blocked"],
|
|
31
|
+
["active", "complete"],
|
|
32
|
+
["active", "budget_limited"],
|
|
33
|
+
["active", "cancelled"],
|
|
34
|
+
["paused", "active"],
|
|
35
|
+
["paused", "cancelled"],
|
|
36
|
+
["blocked", "active"],
|
|
37
|
+
["blocked", "cancelled"],
|
|
38
|
+
];
|
|
39
|
+
const expectedLegalEdge = new Set(EXPECTED_LEGAL_EDGES.map(([from, to]) => `${from}→${to}`));
|
|
40
|
+
|
|
23
41
|
describe("isTerminalStatus", () => {
|
|
24
42
|
it.prop([statusArb])("⟺ 在 {complete, budget_limited, cancelled} 内", (s) => isTerminalStatus(s) === TERMINAL.has(s));
|
|
25
43
|
});
|
|
@@ -29,9 +47,9 @@ describe("isActiveStatus", () => {
|
|
|
29
47
|
});
|
|
30
48
|
|
|
31
49
|
describe("transitionStatus", () => {
|
|
32
|
-
// ⭐
|
|
33
|
-
it.prop([statusArb, statusArb])("完备性:不 throw ⟺ to ∈
|
|
34
|
-
const inTable =
|
|
50
|
+
// ⭐ 完备性(抽样):不 throw ⟺ (from, to) ∈ 期望合法边表(独立 oracle,非循环)
|
|
51
|
+
it.prop([statusArb, statusArb])("完备性:不 throw ⟺ (from,to) ∈ 期望合法边表", (from, to) => {
|
|
52
|
+
const inTable = expectedLegalEdge.has(`${from}→${to}`);
|
|
35
53
|
let threw = false;
|
|
36
54
|
try {
|
|
37
55
|
transitionStatus(from, to);
|
|
@@ -49,6 +67,42 @@ describe("transitionStatus", () => {
|
|
|
49
67
|
});
|
|
50
68
|
});
|
|
51
69
|
|
|
70
|
+
// ── M3 迁移基线(V7 对账矩阵):(from,target) 6×6 穷举 ──────────────
|
|
71
|
+
// 迁移前测存量邻接表实现(基线),迁移后测机器包装(to 等价 + throw 形态);
|
|
72
|
+
// 期望合法边表(expectedLegalEdge,文件顶部)迁移前后同一份。
|
|
73
|
+
|
|
74
|
+
describe("transitionStatus (from,target) 6×6 穷举基线(V7 对账矩阵)", () => {
|
|
75
|
+
for (const from of STATUS_VALUES) {
|
|
76
|
+
for (const target of STATUS_VALUES) {
|
|
77
|
+
const legal = expectedLegalEdge.has(`${from}→${target}`);
|
|
78
|
+
it(`${from} → ${target}:${legal ? `合法,返回 ${target}` : "非法,throw"}`, () => {
|
|
79
|
+
if (legal) {
|
|
80
|
+
expect(transitionStatus(from, target)).toBe(target);
|
|
81
|
+
} else {
|
|
82
|
+
expect(() => transitionStatus(from, target)).toThrow();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── 机器级穷举生成组(D5/D6 每步判据第二款):全状态 × 全事件 ──────────
|
|
90
|
+
// generateTableTests(GOAL_MACHINE) 直测机器表(词表/表行误改、terminal
|
|
91
|
+
// 拒绝语义),与上方包装层 6×6(transitionStatus 行为对账)互为独立防线。
|
|
92
|
+
// 必须在收集阶段(模块顶层)调用——describe/it 不能在测试回调内注册。
|
|
93
|
+
const generatedCases = generateTableTests(GOAL_MACHINE);
|
|
94
|
+
|
|
95
|
+
describe("GOAL_MACHINE 穷举覆盖对账", () => {
|
|
96
|
+
it("全组合 36(6 态 × 6 事件)= 合法 9 + 表外 27 + 不变量破坏 0", () => {
|
|
97
|
+
// 合法 9 ⟺ 上方 EXPECTED_LEGAL_EDGES 的 9 条 (from,to) 边(本表
|
|
98
|
+
// from×event→to 单射,两维度同源对账);覆盖数对账防生成器静默漏覆盖。
|
|
99
|
+
expect(generatedCases).toHaveLength(36);
|
|
100
|
+
expect(generatedCases.filter((c) => c.expectation === "transition")).toHaveLength(9);
|
|
101
|
+
expect(generatedCases.filter((c) => c.expectation === "rejected")).toHaveLength(27);
|
|
102
|
+
expect(generatedCases.filter((c) => c.expectation === "invariant-broken")).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
52
106
|
describe("createGoalState", () => {
|
|
53
107
|
it("默认值:status active + 计数器清零 + 预警 flag false", () => {
|
|
54
108
|
expect(createGoalState("my obj")).toMatchObject({
|
|
@@ -87,9 +141,9 @@ describe("createGoalState", () => {
|
|
|
87
141
|
});
|
|
88
142
|
});
|
|
89
143
|
|
|
90
|
-
// ── E1
|
|
144
|
+
// ── E1 检查:状态变更唯一执行点(.status 裸赋值扫描)──
|
|
91
145
|
|
|
92
|
-
describe("E1
|
|
146
|
+
describe("E1 检查:src 内禁止 .status 裸赋值(必须走 transitionStatus 查表)", () => {
|
|
93
147
|
it("扫描 src/**/*.ts(排除 __tests__):0 命中", () => {
|
|
94
148
|
// 正则逐字来自设计定稿,不得自行修改:
|
|
95
149
|
// (?![=>]) 排除 ==/===/=>(比较与箭头函数形态);(?!\s*transitionStatus\b) 放行白名单查表赋值。
|
|
@@ -118,7 +172,7 @@ describe("E1 守卫:src 内禁止 .status 裸赋值(必须走 transitionStat
|
|
|
118
172
|
walk(srcDir);
|
|
119
173
|
expect(
|
|
120
174
|
offenders,
|
|
121
|
-
`检测到 ${offenders.length} 处 .status 裸赋值(状态变更必须走 transitionStatus 查表,见设计 §5.4 E1 /
|
|
175
|
+
`检测到 ${offenders.length} 处 .status 裸赋值(状态变更必须走 transitionStatus 查表,见设计 §5.4 E1 / goal 状态机声明 engine/goal.ts):\n`
|
|
122
176
|
+ offenders.join("\n"),
|
|
123
177
|
).toEqual([]);
|
|
124
178
|
});
|
package/src/engine/goal.ts
CHANGED
|
@@ -4,8 +4,76 @@
|
|
|
4
4
|
* 零 Pi 依赖。import from "./types"。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { defineStateMachine } from "@zhushanwen/state-machine";
|
|
8
|
+
|
|
7
9
|
import type { BudgetConfig, GoalRuntimeState, GoalStatus } from "./types";
|
|
8
|
-
import { DEFAULT_BUDGET, TERMINAL_GOAL_STATUSES
|
|
10
|
+
import { DEFAULT_BUDGET, TERMINAL_GOAL_STATUSES } from "./types";
|
|
11
|
+
|
|
12
|
+
// ── Goal 状态机声明(@zhushanwen/state-machine 行式表,M3 迁移)────────
|
|
13
|
+
// 事件词表从 transitionStatus 调用点目标态语义派生(D6 M3):
|
|
14
|
+
// - pause / report_blocked / hit_budget_limit / complete / cancel = 用户或 agent
|
|
15
|
+
// 的领域动作(/goal pause、goal_control report_blocked、token 预算耗尽、
|
|
16
|
+
// goal_control complete、/goal clear);
|
|
17
|
+
// - cancel 与 resume 各覆盖多源态(clear/resume 对 paused、blocked 对称)。
|
|
18
|
+
// 终态标志与 TERMINAL_GOAL_STATUSES(types.ts,域查询用)是同一事实的两处
|
|
19
|
+
// 声明。表行为由两层穷举锁定(__tests__/goal.test.ts):机器级
|
|
20
|
+
// generateTableTests 全状态×全事件生成组(D5/D6)+ 包装层 (from,target)
|
|
21
|
+
// 6×6 基线(V7 对账矩阵)。新增状态或转移时必须同步更新此表与两层基线
|
|
22
|
+
// (forcing function)。
|
|
23
|
+
|
|
24
|
+
/** Goal 机器事件词表(类型层与下方 events 声明键集互为对账)。 */
|
|
25
|
+
type GoalEvent =
|
|
26
|
+
| "pause"
|
|
27
|
+
| "report_blocked"
|
|
28
|
+
| "complete"
|
|
29
|
+
| "hit_budget_limit"
|
|
30
|
+
| "cancel"
|
|
31
|
+
| "resume";
|
|
32
|
+
|
|
33
|
+
export const GOAL_MACHINE = defineStateMachine<GoalStatus, GoalEvent>({
|
|
34
|
+
name: "goal",
|
|
35
|
+
states: {
|
|
36
|
+
active: {},
|
|
37
|
+
paused: {},
|
|
38
|
+
blocked: {},
|
|
39
|
+
complete: { terminal: true },
|
|
40
|
+
budget_limited: { terminal: true },
|
|
41
|
+
cancelled: { terminal: true },
|
|
42
|
+
},
|
|
43
|
+
initial: "active",
|
|
44
|
+
events: {
|
|
45
|
+
pause: {},
|
|
46
|
+
report_blocked: {},
|
|
47
|
+
complete: {},
|
|
48
|
+
hit_budget_limit: {},
|
|
49
|
+
cancel: {},
|
|
50
|
+
resume: {},
|
|
51
|
+
},
|
|
52
|
+
transitions: [
|
|
53
|
+
{ from: "active", on: "pause", to: "paused" },
|
|
54
|
+
{ from: "active", on: "report_blocked", to: "blocked" },
|
|
55
|
+
{ from: "active", on: "complete", to: "complete" },
|
|
56
|
+
{ from: "active", on: "hit_budget_limit", to: "budget_limited" },
|
|
57
|
+
{ from: ["active", "paused", "blocked"], on: "cancel", to: "cancelled" },
|
|
58
|
+
{ from: ["paused", "blocked"], on: "resume", to: "active" },
|
|
59
|
+
],
|
|
60
|
+
errorPolicy: "throw",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** (from, target) → 事件名索引:从机器行式表派生,transitionStatus 据此查边。 */
|
|
64
|
+
const edgeEventIndex = new Map<GoalStatus, Map<GoalStatus, GoalEvent>>();
|
|
65
|
+
for (const row of GOAL_MACHINE.definition.transitions) {
|
|
66
|
+
if (row.to === null) continue; // goal 域无实体消亡行(to: null)
|
|
67
|
+
const froms = Array.isArray(row.from) ? row.from : [row.from];
|
|
68
|
+
for (const from of froms) {
|
|
69
|
+
let byTarget = edgeEventIndex.get(from);
|
|
70
|
+
if (!byTarget) {
|
|
71
|
+
byTarget = new Map<GoalStatus, GoalEvent>();
|
|
72
|
+
edgeEventIndex.set(from, byTarget);
|
|
73
|
+
}
|
|
74
|
+
byTarget.set(row.to, row.on);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
9
77
|
|
|
10
78
|
export function isTerminalStatus(status: GoalStatus): boolean {
|
|
11
79
|
return TERMINAL_GOAL_STATUSES.has(status);
|
|
@@ -16,18 +84,26 @@ export function isActiveStatus(status: GoalStatus): boolean {
|
|
|
16
84
|
}
|
|
17
85
|
|
|
18
86
|
/**
|
|
19
|
-
*
|
|
87
|
+
* 安全的状态转换(M3 迁移后形态:机器保形包装)。
|
|
20
88
|
*
|
|
89
|
+
* 签名与语义不变:查 (current, next) 边的事件名喂机器 transition 并断言
|
|
90
|
+
* to === next;表外 (current, next) throw(消息与迁移前一致)。
|
|
21
91
|
* 调用方必须在终态转换前自行检查 isTerminalStatus(如 finalizeAndPersist),
|
|
22
92
|
* 因为终态→终态不是合法转换(表为空)。对 active→终态、active→paused/blocked、
|
|
23
93
|
* paused/blocked→active/cancelled 这些合法路径直接返回 next。
|
|
24
94
|
*/
|
|
25
95
|
export function transitionStatus(current: GoalStatus, next: GoalStatus): GoalStatus {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
96
|
+
const event = edgeEventIndex.get(current)?.get(next);
|
|
97
|
+
if (event === undefined) {
|
|
98
|
+
throw new Error(`Invalid goal state transition: ${current} → ${next}`);
|
|
99
|
+
}
|
|
100
|
+
const result = GOAL_MACHINE.transition(current, event);
|
|
101
|
+
if (result.state !== next) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Goal state machine invariant broken: transition(${current}, ${event}) returned ${String(result.state)}, expected ${next}.`,
|
|
104
|
+
);
|
|
29
105
|
}
|
|
30
|
-
|
|
106
|
+
return next;
|
|
31
107
|
}
|
|
32
108
|
|
|
33
109
|
/**
|
package/src/engine/liveness.ts
CHANGED
|
@@ -78,14 +78,14 @@ const BACKOFF_MULTIPLIER = 2;
|
|
|
78
78
|
* message.toolCalls 顶层字段从未存在)。这里只数块个数——调用方以「相邻两次
|
|
79
79
|
* agent_end 的差分」判定本 turn 是否有真实工具活动,不需要 name/arguments。
|
|
80
80
|
*
|
|
81
|
-
* duck-typed
|
|
81
|
+
* duck-typed 逐层检查(对齐 pending-notifications state.ts 的 EntryLike 风格),
|
|
82
82
|
* 不依赖 SDK 具体类型;畸形 entry 跳过不计。
|
|
83
83
|
*/
|
|
84
84
|
export function countToolCallBlocks(entries: unknown[]): number {
|
|
85
85
|
let count = 0;
|
|
86
86
|
for (const raw of entries) {
|
|
87
87
|
if (!raw || typeof raw !== "object") continue;
|
|
88
|
-
// 断言目标带必填 unknown 字段(typeof object
|
|
88
|
+
// 断言目标带必填 unknown 字段(typeof object 已检查),非全可选形状
|
|
89
89
|
const entry = raw as { type: unknown; message: unknown };
|
|
90
90
|
if (entry.type !== "message") continue;
|
|
91
91
|
if (!entry.message || typeof entry.message !== "object") continue;
|
package/src/engine/types.ts
CHANGED
|
@@ -22,19 +22,6 @@ export const TERMINAL_GOAL_STATUSES: ReadonlySet<GoalStatus> = new Set([
|
|
|
22
22
|
"cancelled",
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* 显式状态转换表(system-architecture §5)。终态映射空数组——不可逆。
|
|
27
|
-
* transitionStatus 据此查表,非法转换 throw。新增状态时必须更新此表(forcing function)。
|
|
28
|
-
*/
|
|
29
|
-
export const VALID_TRANSITIONS: Record<GoalStatus, GoalStatus[]> = {
|
|
30
|
-
active: ["paused", "blocked", "complete", "budget_limited", "cancelled"],
|
|
31
|
-
paused: ["active", "cancelled"],
|
|
32
|
-
blocked: ["active", "cancelled"],
|
|
33
|
-
complete: [],
|
|
34
|
-
budget_limited: [],
|
|
35
|
-
cancelled: [],
|
|
36
|
-
};
|
|
37
|
-
|
|
38
25
|
// ── 预算配置 ────────────────────────────────────────
|
|
39
26
|
|
|
40
27
|
export interface BudgetConfig {
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* FR-4.2/D-16:ctx 必填,移除 lastCtx 模块级可变状态。
|
|
16
16
|
* FR-6.4:移除 hasPendingInjection。
|
|
17
|
-
* FR-6.7:移除 pendingPause(ESC 改用 ctx.signal.aborted
|
|
17
|
+
* FR-6.7:移除 pendingPause(ESC 改用 ctx.signal.aborted 检查,在各 event handler)。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, MessageEndEvent } from "@earendil-works/pi-coding-agent";
|
|
@@ -104,7 +104,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
104
104
|
updateWidget(session, ports.ui);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
// MF-R2-1
|
|
107
|
+
// MF-R2-1 从根源修复:session_shutdown 在 runner invalidate 之前触发(此刻 pi/ctx
|
|
108
108
|
// 仍可用),取消待发的退避 continuation timer——覆盖 reload / new / resume /
|
|
109
109
|
// fork / quit 全部失效路径,旧 timer 不再携 stale ctx 闭包存活到到期
|
|
110
110
|
pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
|
|
@@ -138,7 +138,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
138
138
|
* FR-4.2/D-16: ctx 必填(消除 lastCtx 模块级可变状态)。
|
|
139
139
|
* ports 构造复用 adapters/ports.buildPorts(DRY:单一 ports 构造点)。
|
|
140
140
|
*
|
|
141
|
-
* 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof
|
|
141
|
+
* 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 检查
|
|
142
142
|
* 读取 `globalThis[Symbol.for("@zhushanwen/pi-goal.goalInit")]`(plan/src/compact.ts)。
|
|
143
143
|
* 不挂 pi API 对象:pi 为每个扩展创建独立 ExtensionAPI(1.0.0 复核同构),挂 pi 对象的字段
|
|
144
144
|
* 跨扩展不可见。
|
package/src/persistence.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function serializeState(state: GoalRuntimeState): GoalRuntimeState {
|
|
|
31
31
|
* npm pi-goal 0.7.x(base 968b9d76)经 finalizeAndPersist 真实写入过 `time_limited`(时间预算
|
|
32
32
|
* 维度,新状态机已删除该状态)。若不归一化,升级用户持有该状态的 goal 会功能死锁:
|
|
33
33
|
* /goal clear 的 transitionStatus 查表 throw、goal_control create 误报 already active、
|
|
34
|
-
* resume/update 被 isActiveStatus
|
|
34
|
+
* resume/update 被 isActiveStatus 检查拒绝——唯一出口是手动删 session 文件。
|
|
35
35
|
* 归一化为语义最接近的 `budget_limited`(预算耗尽终态),clear 走 isTerminalStatus 快速路径。
|
|
36
36
|
* 其余值原样透传(不做值域校验,保持现状)。
|
|
37
37
|
*/
|
package/src/ports.ts
CHANGED
|
@@ -53,8 +53,8 @@ export interface UiPort {
|
|
|
53
53
|
/**
|
|
54
54
|
* 设置 widget(双模单调用)。content = { gui, text } 双模 payload / undefined 清除;
|
|
55
55
|
* 模式分派(RPC→gui 经 marker 通道 / TUI→text 原生文本行)由 protocol setWidgetDual
|
|
56
|
-
*
|
|
57
|
-
* hasUI
|
|
56
|
+
* 单点内化(检查单点化说明见 extension-protocol core/helpers.ts,adapter 委托 helper)。
|
|
57
|
+
* hasUI 检查留在调用方(FR-6.6)。
|
|
58
58
|
*/
|
|
59
59
|
setWidget(name: string, content: DualWidgetContent | undefined): void;
|
|
60
60
|
/** 设置 status bar */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - renderStatusLine: slug 标题(fallback objective 截断)+ 状态后缀 + cancelled 短路 + token/耗时显示
|
|
7
7
|
* - renderWidgetLines: cancelled 短路 + 标题行 + criteria 摘要单行 + 预算进度条(Objective 全文行已移除,GAP-8)
|
|
8
8
|
* - renderTerminalStatusLine: 终态单行 + cancelled 短路
|
|
9
|
-
* - updateWidget: FR-6.6 hasUI
|
|
9
|
+
* - updateWidget: FR-6.6 hasUI 检查 + 终态折叠 + cancelled 清除
|
|
10
10
|
*
|
|
11
11
|
* 用 passthrough ThemeLike(fg/bold 返回原文),断言渲染逻辑而非颜色。
|
|
12
12
|
*/
|
|
@@ -197,7 +197,7 @@ describe("renderWidgetLines", () => {
|
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
// ── updateWidget(FR-6.6 hasUI
|
|
200
|
+
// ── updateWidget(FR-6.6 hasUI 检查 + dual payload)──
|
|
201
201
|
|
|
202
202
|
interface RecordedCall {
|
|
203
203
|
method: "setWidget" | "setStatus";
|
|
@@ -225,7 +225,7 @@ function makeUiPort(hasUI: boolean): { ui: UiPort; calls: RecordedCall[] } {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
describe("updateWidget (FR-6.6 hasUI guard)", () => {
|
|
228
|
-
it("hasUI=false → 不调 setWidget/setStatus(headless
|
|
228
|
+
it("hasUI=false → 不调 setWidget/setStatus(headless 检查)", () => {
|
|
229
229
|
const { ui, calls } = makeUiPort(false);
|
|
230
230
|
const session = createGoalSession();
|
|
231
231
|
session.state = makeState({ status: "active" });
|
package/src/projection/widget.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - 类型 import 自 engine/types.ts 与 ports.ts(ThemeLike 上移至接口层)
|
|
7
7
|
* - 工具函数 import 自 engine/budget.ts
|
|
8
8
|
* - 时间计算基于 state.timeUsedSeconds(不含 Date.now() 副作用段)
|
|
9
|
-
* - updateWidget(session, uiPort) 含 FR-6.6 hasUI
|
|
9
|
+
* - updateWidget(session, uiPort) 含 FR-6.6 hasUI 检查
|
|
10
10
|
* - 仅 token 维度预算(time budget 已移除);耗时仅作记账显示
|
|
11
11
|
*
|
|
12
12
|
* slug 精简(widget 显示优化):
|
|
@@ -184,7 +184,7 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
|
|
|
184
184
|
return lines;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// ── updateWidget(FR-6.6 hasUI
|
|
187
|
+
// ── updateWidget(FR-6.6 hasUI 检查)──
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* 刷新 widget + status bar。
|
package/src/service.ts
CHANGED
|
@@ -154,14 +154,14 @@ export function createGoal(
|
|
|
154
154
|
/**
|
|
155
155
|
* 唯一终态序列入口(FR-3.3 / AC-3)。
|
|
156
156
|
*
|
|
157
|
-
*
|
|
157
|
+
* 收尾所有 active→terminal 转换的完整副作用序列——service(budget 终态)与
|
|
158
158
|
* command-adapter(clear / resume 超预算)、goal-control-adapter(complete)共用
|
|
159
159
|
* 同一 `transitionStatus + completedAtTurnIndex + writeHistory + appendState` 序列。
|
|
160
160
|
*
|
|
161
161
|
* 序列(严格顺序):
|
|
162
162
|
* 1. tickState(state)(FR-6.5:转 terminal 前累加当前运行段——此时 status 仍为 active)
|
|
163
163
|
* 2. finalizeGoal(state, terminalStatus, ports)
|
|
164
|
-
* — transitionStatus(
|
|
164
|
+
* — transitionStatus(终态检查) + completedAtTurnIndex= + appendHistory(FR-8.7 矩阵)
|
|
165
165
|
* 3. ports.persistence.appendState(serializeState(state))(持久化终态 state)
|
|
166
166
|
*
|
|
167
167
|
* @param state runtime state(mutate)
|
|
@@ -255,10 +255,10 @@ export function applyEvent(
|
|
|
255
255
|
|
|
256
256
|
switch (eventType) {
|
|
257
257
|
case "message_end": {
|
|
258
|
-
// token 累加(FR-8.6 G-R2-001)—— 仅 active 时累加(回归修复:原缺 isActiveStatus
|
|
258
|
+
// token 累加(FR-8.6 G-R2-001)—— 仅 active 时累加(回归修复:原缺 isActiveStatus 检查,
|
|
259
259
|
// blocked 等 non-active 状态会错误累加 token)。复用 engine 纯函数。
|
|
260
260
|
if (!isActiveStatus(session.state.status)) break;
|
|
261
|
-
//
|
|
261
|
+
// 运行时检查解析事件数据(未知形状 → 可选字段),替代全可选结构断言
|
|
262
262
|
const data = toMessageEndData(eventData);
|
|
263
263
|
if (data?.message?.role !== "assistant") break;
|
|
264
264
|
const usage = data.message.usage;
|
package/src/session.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* U6c:重建输入接活跃路径裁剪——被撤子树的 goal-state 不进重建态)。
|
|
7
7
|
*
|
|
8
8
|
* FR-6.4: 删除 hasPendingInjection(僵尸字段)
|
|
9
|
-
* FR-6.7: 删除 pendingPause(ESC 改用 aborted
|
|
9
|
+
* FR-6.7: 删除 pendingPause(ESC 改用 aborted 检查)
|
|
10
10
|
* FR-8.1 G-006: session append-only——entry GC 不生效(生产),history 显示侧截断
|
|
11
11
|
* FR-3: 崩溃后保持原状态(active 重启计时;paused/blocked 保持;终态保持)
|
|
12
12
|
*/
|
|
@@ -26,7 +26,7 @@ export interface GoalSession {
|
|
|
26
26
|
/**
|
|
27
27
|
* W5 退避延迟发送的 continuation 定时器(无进展退避时 continuation 不立即发出,
|
|
28
28
|
* 按间隔 ×2 递增延迟)。非 null 表示有一个待发的退避 continuation——调度前必须
|
|
29
|
-
*
|
|
29
|
+
* 先清(防双发)。清理侧(cancelContinuationTimer 统一入口):clearGoalSession、
|
|
30
30
|
* session_start(新 session 生命周期旧 timer 作废)、before_agent_start(新用户
|
|
31
31
|
* 活动使旧退避作废)、下一轮 agent_end 调度前。
|
|
32
32
|
*/
|
|
@@ -71,7 +71,7 @@ export interface ActivePathSessionView {
|
|
|
71
71
|
// ── reconstructGoalState(session_start / session_tree 时调)──────────
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* 从 session entries 恢复 goal state(U6c:重建输入 =
|
|
74
|
+
* 从 session entries 恢复 goal state(U6c:重建输入 = 活跃路径派生视图,非全文件)。
|
|
75
75
|
*
|
|
76
76
|
* Pi SDK 的 session 是 append-only,getEntries() 返回 filter-copy——splice
|
|
77
77
|
* 无法修改真实 entries。Entry GC(goal-state 留 1、goal-history 留 20)在生产
|
|
@@ -127,7 +127,7 @@ function isGoalStateEntry(entry: SessionEntryLike): boolean {
|
|
|
127
127
|
// ── clearGoalSession ──────────────────────────────────
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* 取消待发的退避 continuation timer
|
|
130
|
+
* 取消待发的退避 continuation timer(全部清理点走本函数,防止清理侧遗漏):
|
|
131
131
|
* - clearGoalSession:goal 已终态/清除,发出即僵尸 turn
|
|
132
132
|
* - session_start:session 句柄跨 session 复用,新 session 生命周期旧 timer 作废
|
|
133
133
|
* - before_agent_start:新用户活动使旧退避作废,该轮 agent_end 按最新状态重新决策
|
|
@@ -145,7 +145,7 @@ export function clearGoalSession(session: GoalSession, uiPort: UiPort): void {
|
|
|
145
145
|
cancelContinuationTimer(session);
|
|
146
146
|
session.state = null;
|
|
147
147
|
session.isProcessing = false;
|
|
148
|
-
// FR-6.6: hasUI
|
|
148
|
+
// FR-6.6: hasUI 检查
|
|
149
149
|
if (uiPort.hasUI) {
|
|
150
150
|
uiPort.setWidget("goal", undefined);
|
|
151
151
|
uiPort.setStatus("goal", undefined);
|