@zhushanwen/pi-goal 0.8.0 → 0.8.1
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.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -106,6 +106,14 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
|
|
|
106
106
|
handleComplete({ action: "complete", evidence: " " }, session, makeFakePorts()),
|
|
107
107
|
).toThrow(/evidence/);
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
it("evidence undefined → throw(schema 降级后 handler 唯一兜底:'required' 错误)", () => {
|
|
111
|
+
const session = createGoalSession();
|
|
112
|
+
session.state = activeState();
|
|
113
|
+
expect(() => handleComplete({ action: "complete" }, session, makeFakePorts())).toThrow(
|
|
114
|
+
/evidence.*required/,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
109
117
|
});
|
|
110
118
|
|
|
111
119
|
// ── handleCreate ─────────────────────────────────────
|
|
@@ -142,6 +150,13 @@ describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal"
|
|
|
142
150
|
).toThrow(/objective/);
|
|
143
151
|
});
|
|
144
152
|
|
|
153
|
+
it("objective undefined → throw(schema 降级后 handler 唯一兜底:'required' 错误)", () => {
|
|
154
|
+
const session = createGoalSession();
|
|
155
|
+
expect(() => handleCreate({ action: "create", successCriteria: "y" }, session, makeFakePorts())).toThrow(
|
|
156
|
+
/objective.*required/,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
145
160
|
it("successCriteria 空 → throw(即使有 slug + objective)", () => {
|
|
146
161
|
const session = createGoalSession();
|
|
147
162
|
expect(() =>
|
|
@@ -153,6 +168,13 @@ describe("handleCreate — slug+objective 必填 + 非终态守卫 + createGoal"
|
|
|
153
168
|
).toThrow(/successCriteria/);
|
|
154
169
|
});
|
|
155
170
|
|
|
171
|
+
it("successCriteria undefined → throw(schema 降级后 handler 唯一兜底:'required' 错误)", () => {
|
|
172
|
+
const session = createGoalSession();
|
|
173
|
+
expect(() => handleCreate({ action: "create", objective: "x" }, session, makeFakePorts())).toThrow(
|
|
174
|
+
/successCriteria.*required/,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
156
178
|
const NON_TERMINAL_STATUSES: GoalRuntimeState["status"][] = ["active", "paused", "blocked"];
|
|
157
179
|
it.each(NON_TERMINAL_STATUSES)("已有 %s goal → throw(D25 非终态守卫)", (status) => {
|
|
158
180
|
const session = createGoalSession();
|
|
@@ -250,4 +272,12 @@ describe("handleReportBlocked — active 守卫 + tick + transition + persist",
|
|
|
250
272
|
handleReportBlocked({ action: "report_blocked", reason: "" }, session, makeFakePorts()),
|
|
251
273
|
).toThrow(/reason/);
|
|
252
274
|
});
|
|
275
|
+
|
|
276
|
+
it("reason undefined → throw(schema 降级后 handler 唯一兜底:'required' 错误)", () => {
|
|
277
|
+
const session = createGoalSession();
|
|
278
|
+
session.state = activeState();
|
|
279
|
+
expect(() => handleReportBlocked({ action: "report_blocked" }, session, makeFakePorts())).toThrow(
|
|
280
|
+
/reason.*required/,
|
|
281
|
+
);
|
|
282
|
+
});
|
|
253
283
|
});
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* goal_control schema —
|
|
2
|
+
* goal_control schema — 扁平 Object 校验测试(OpenAI 兼容)
|
|
3
3
|
*
|
|
4
|
-
* schema
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* schema 为扁平 Type.Object + action 字段级 Type.Union(等价 enum)+ 各字段 Optional +
|
|
5
|
+
* additionalProperties:false。OpenAI function calling 要求 parameters 顶层 type:"object",
|
|
6
|
+
* 顶层 Type.Union 序列化后只有 anyOf 无 type 字段,会被严格 OpenAI 兼容网关 400 拒绝
|
|
7
|
+
* 整个会话——故采用扁平结构(范式对齐 scheduler ScheduleControlParams)。
|
|
8
|
+
*
|
|
9
|
+
* typebox schema 对象即序列化形态:GoalControlParams.type 就是发往 provider 的 JSON 顶层 type。
|
|
10
|
+
* pi 生产校验器为 typebox/compile 的 Compile(schema).Check(args),与此处 Value.Check 同源,
|
|
11
|
+
* 故用 Value.Check 验证。
|
|
12
|
+
*
|
|
13
|
+
* 分支隔离从 schema 层降级为运行时 handler 字段存在性校验(见 goal-control-adapter.test.ts)。
|
|
7
14
|
*/
|
|
8
15
|
import { describe, expect, it } from "vitest";
|
|
9
16
|
import { Value } from "typebox/value";
|
|
10
17
|
|
|
11
18
|
import { GoalControlParams } from "../adapters/goal-control-adapter";
|
|
12
19
|
|
|
13
|
-
describe("GoalControlParams —
|
|
14
|
-
//
|
|
20
|
+
describe("GoalControlParams — 扁平 Object(OpenAI 兼容)", () => {
|
|
21
|
+
// 顶层合规:序列化形态顶层必须有 type:"object",不得有 anyOf(否则被严格网关 400 拒绝)
|
|
22
|
+
it("顶层 type === 'object' 且无 anyOf(OpenAI function calling 合规)", () => {
|
|
23
|
+
expect(GoalControlParams.type).toBe("object");
|
|
24
|
+
expect(GoalControlParams.anyOf).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// valid:各 action 完整 + optional 字段 → 通过
|
|
15
28
|
it.each([
|
|
16
29
|
["create 完整", { action: "create", objective: "x", successCriteria: "y" }],
|
|
17
30
|
["create + slug", { action: "create", slug: "refactor-auth", objective: "x", successCriteria: "y" }],
|
|
@@ -22,16 +35,19 @@ describe("GoalControlParams — discriminated union(TC6)", () => {
|
|
|
22
35
|
expect(Value.Check(GoalControlParams, params)).toBe(true);
|
|
23
36
|
});
|
|
24
37
|
|
|
25
|
-
//
|
|
38
|
+
// 关键语义变更:扁平化后所有声明的字段(含跨 action 的)均为 Optional,
|
|
39
|
+
// schema 不再做分支隔离。{action:"complete", evidence, objective} 放行
|
|
40
|
+
// (objective 是声明的 Optional 字段,schema 层无法区分它属于哪个 action)。
|
|
41
|
+
// 分支隔离降级为运行时 handler 字段存在性校验,见 goal-control-adapter.test.ts。
|
|
42
|
+
it("跨 action 字段(complete + objective)现在通过(分支隔离降级为运行时校验)", () => {
|
|
43
|
+
expect(Value.Check(GoalControlParams, { action: "complete", evidence: "x", objective: "leak" })).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// invalid:缺 action / 未知 action / 额外字段(additionalProperties:false)
|
|
26
47
|
it.each([
|
|
27
|
-
["
|
|
28
|
-
["
|
|
48
|
+
["缺 action", { objective: "x", successCriteria: "y" }],
|
|
49
|
+
["未知 action(不在 union 内)", { action: "unknown" }],
|
|
29
50
|
["create 含额外字段(additionalProperties:false)", { action: "create", objective: "x", successCriteria: "y", foo: 1 }],
|
|
30
|
-
["create 含 complete 分支字段 evidence(分支隔离)", { action: "create", objective: "x", successCriteria: "y", evidence: "leak" }],
|
|
31
|
-
["complete 缺 evidence", { action: "complete" }],
|
|
32
|
-
["report_blocked 缺 reason", { action: "report_blocked" }],
|
|
33
|
-
["未知 action", { action: "unknown" }],
|
|
34
|
-
["缺 action", { objective: "x" }],
|
|
35
51
|
])("invalid: %s → 拒绝", (_name, params) => {
|
|
36
52
|
expect(Value.Check(GoalControlParams, params)).toBe(false);
|
|
37
53
|
});
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* create 不调 sendUserMessage:toolcall 时 AI 已在 turn 中,返回结果后自行续跑
|
|
20
20
|
* (与 /goal set 的 followUp 触发区分;对齐 Codex create_goal 不自动续跑)。
|
|
21
21
|
*
|
|
22
|
-
* schema
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* schema:扁平 Type.Object(OpenAI 兼容,C3)——parameters 顶层必须是 type:"object",
|
|
23
|
+
* 顶层 Type.Union 序列化后只有 anyOf 无 type,会被严格 OpenAI 兼容网关 400 拒绝。故采用
|
|
24
|
+
* 扁平 Object + action 字段级 Type.Union(等价 enum)+ 各字段 Optional + additionalProperties:false。
|
|
25
|
+
* 分支隔离从 schema 层降级为运行时 handler 字段存在性校验(见各 handler 开头);
|
|
26
|
+
* pi 生产校验器为 typebox/compile 的 Compile(schema).Check(args),缺失/错误 action、额外字段
|
|
27
|
+
* 在 schema 层拒绝,缺必填字段由 handler 校验兜底。
|
|
26
28
|
*
|
|
27
29
|
* executionMode: "sequential"——状态变更 tool,不可与同批其他 tool 并行执行。
|
|
28
30
|
*
|
|
@@ -46,62 +48,60 @@ import { buildPorts } from "./ports";
|
|
|
46
48
|
// ── Params schema(discriminated union,C3)────────────
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
* goal_control 参数 schema
|
|
50
|
-
*
|
|
51
|
+
* goal_control 参数 schema。扁平 Type.Object + action 字段级 Type.Union(等价 enum)。
|
|
52
|
+
*
|
|
53
|
+
* OpenAI function calling 规范要求 parameters 顶层必须是 type:"object";顶层 Type.Union
|
|
54
|
+
* 序列化后只有 anyOf 无 type 字段,会被严格的 OpenAI 兼容网关 400 拒绝整个会话。故采用
|
|
55
|
+
* 扁平 Object + action enum,分支隔离从 schema 层降级为运行时 handler 字段存在性校验
|
|
56
|
+
* (见各 handler 开头)。范式对齐 scheduler ScheduleControlParams。
|
|
57
|
+
*
|
|
58
|
+
* additionalProperties:false 阻挡未知字段;缺失/错误 action 仍由 schema 层拒绝。
|
|
51
59
|
*/
|
|
52
|
-
const
|
|
60
|
+
export const GoalControlParams = Type.Object(
|
|
53
61
|
{
|
|
54
|
-
action: Type.
|
|
62
|
+
action: Type.Union(
|
|
63
|
+
[Type.Literal("create"), Type.Literal("complete"), Type.Literal("report_blocked")],
|
|
64
|
+
{ description: "create | complete | report_blocked" },
|
|
65
|
+
),
|
|
55
66
|
slug: Type.Optional(
|
|
56
67
|
Type.String({
|
|
57
68
|
description: "可选。状态栏标题用的短 kebab-case 标识,仅用于显示,不注入 prompt。",
|
|
58
69
|
}),
|
|
59
70
|
),
|
|
60
|
-
objective: Type.
|
|
61
|
-
|
|
71
|
+
objective: Type.Optional(
|
|
72
|
+
Type.String({
|
|
73
|
+
description:
|
|
62
74
|
"必填。用你自己的话重述真实目标——用户实际想要达成什么,而非字面复述请求。模糊时推断最可能的意图并明确陈述。",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
successCriteria: Type.Optional(
|
|
78
|
+
Type.String({
|
|
79
|
+
description:
|
|
66
80
|
"必填。可检查的完成条件(如「测试通过」「文件 X 存在且含内容 Y」「命令 Z 输出 W」),不是愿景或「能用就行」。这是 complete 前必须逐条满足的门槛。",
|
|
67
|
-
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
68
83
|
tokenBudget: Type.Optional(
|
|
69
84
|
Type.Number({
|
|
70
85
|
description: "可选。新目标的 token 预算(正数)。除非用户指定,否则省略。",
|
|
71
86
|
}),
|
|
72
87
|
),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const CompleteParams = Type.Object(
|
|
78
|
-
{
|
|
79
|
-
action: Type.Literal("complete"),
|
|
80
|
-
evidence: Type.String({
|
|
81
|
-
description:
|
|
88
|
+
evidence: Type.Optional(
|
|
89
|
+
Type.String({
|
|
90
|
+
description:
|
|
82
91
|
"必填。具体完成证据(改动/新建的文件、通过的测试、运行的命令)。不要基于假设、意图或部分进度标记完成。",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const ReportBlockedParams = Type.Object(
|
|
89
|
-
{
|
|
90
|
-
action: Type.Literal("report_blocked"),
|
|
91
|
-
reason: Type.String({
|
|
92
|
-
description:
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
reason: Type.Optional(
|
|
95
|
+
Type.String({
|
|
96
|
+
description:
|
|
93
97
|
"必填。具体阻塞条件及已尝试的方案。不要用于不确定、困难、缓慢或未完成的工作——继续做。",
|
|
94
|
-
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
95
100
|
},
|
|
96
101
|
{ additionalProperties: false },
|
|
97
102
|
);
|
|
98
103
|
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
export type CreateActionParams = Static<typeof CreateParams>;
|
|
102
|
-
export type CompleteActionParams = Static<typeof CompleteParams>;
|
|
103
|
-
export type ReportBlockedActionParams = Static<typeof ReportBlockedParams>;
|
|
104
|
-
export type GoalControlActionParams = Static<typeof GoalControlParams>;
|
|
104
|
+
export type GoalControlParamsT = Static<typeof GoalControlParams>;
|
|
105
105
|
|
|
106
106
|
// ── Details(renderResult 数据来源)──────────────────
|
|
107
107
|
|
|
@@ -131,13 +131,16 @@ export interface GoalControlDetails {
|
|
|
131
131
|
* 终态旧 goal 走 createGoal 快速路径覆盖(createGoal 内部 active 守卫,终态可覆盖)。
|
|
132
132
|
*/
|
|
133
133
|
export function handleCreate(
|
|
134
|
-
params:
|
|
134
|
+
params: GoalControlParamsT,
|
|
135
135
|
session: GoalSession,
|
|
136
136
|
ports: ServicePorts,
|
|
137
137
|
): GoalControlDetails {
|
|
138
|
+
if (params.objective === undefined) {
|
|
139
|
+
throw new Error("'objective' required for create. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":\"...\"}");
|
|
140
|
+
}
|
|
138
141
|
const objective = params.objective.trim();
|
|
139
142
|
if (!objective) {
|
|
140
|
-
// schema
|
|
143
|
+
// schema 已放行(扁平化后 optional),此处挡空串(LLM 可能传空串)
|
|
141
144
|
throw new Error(
|
|
142
145
|
"'objective' must not be empty. Describe the concrete objective to pursue. Correct: {\"action\":\"create\",\"slug\":\"<kebab-case>\",\"objective\":\"<concrete objective>\",\"successCriteria\":\"<checkable conditions>\"}",
|
|
143
146
|
);
|
|
@@ -145,6 +148,9 @@ export function handleCreate(
|
|
|
145
148
|
// slug 真 optional(TC11):缺失时 fallback goalId 截断(与 buildGoalGui 口径一致),不强制必填
|
|
146
149
|
const slugInput = params.slug?.trim();
|
|
147
150
|
|
|
151
|
+
if (params.successCriteria === undefined) {
|
|
152
|
+
throw new Error("'successCriteria' required for create. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":\"...\"}");
|
|
153
|
+
}
|
|
148
154
|
const successCriteria = params.successCriteria.trim();
|
|
149
155
|
if (!successCriteria) {
|
|
150
156
|
throw new Error(
|
|
@@ -192,7 +198,7 @@ export function handleCreate(
|
|
|
192
198
|
* 全解耦后不再做 todo 完成前置检查——todo 是否全完成由 AI 自行判断(prompt 软建议)。
|
|
193
199
|
*/
|
|
194
200
|
export function handleComplete(
|
|
195
|
-
params:
|
|
201
|
+
params: GoalControlParamsT,
|
|
196
202
|
session: GoalSession,
|
|
197
203
|
ports: ServicePorts,
|
|
198
204
|
): GoalControlDetails {
|
|
@@ -201,6 +207,9 @@ export function handleComplete(
|
|
|
201
207
|
if (!isActiveStatus(state.status)) {
|
|
202
208
|
throw new Error(`Goal is not active (status: ${state.status}). Only an active goal can be completed.`);
|
|
203
209
|
}
|
|
210
|
+
if (params.evidence === undefined) {
|
|
211
|
+
throw new Error("'evidence' required for complete. Correct: {\"action\":\"complete\",\"evidence\":\"...\"}");
|
|
212
|
+
}
|
|
204
213
|
const evidence = params.evidence.trim();
|
|
205
214
|
if (!evidence) {
|
|
206
215
|
throw new Error(
|
|
@@ -223,7 +232,7 @@ export function handleComplete(
|
|
|
223
232
|
* 否则转 blocked 后 persistState 内部的 tick 因 status≠active 不累加,丢失最后一段运行时间。
|
|
224
233
|
*/
|
|
225
234
|
export function handleReportBlocked(
|
|
226
|
-
params:
|
|
235
|
+
params: GoalControlParamsT,
|
|
227
236
|
session: GoalSession,
|
|
228
237
|
ports: ServicePorts,
|
|
229
238
|
): GoalControlDetails {
|
|
@@ -232,6 +241,9 @@ export function handleReportBlocked(
|
|
|
232
241
|
if (state.status !== "active") {
|
|
233
242
|
throw new Error(`Goal is not active (status: ${state.status}). Only an active goal can report_blocked.`);
|
|
234
243
|
}
|
|
244
|
+
if (params.reason === undefined) {
|
|
245
|
+
throw new Error("'reason' required for report_blocked. Correct: {\"action\":\"report_blocked\",\"reason\":\"...\"}");
|
|
246
|
+
}
|
|
235
247
|
const reason = params.reason.trim();
|
|
236
248
|
if (!reason) {
|
|
237
249
|
throw new Error(
|
|
@@ -297,7 +309,7 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
297
309
|
|
|
298
310
|
async execute(
|
|
299
311
|
_toolCallId: string,
|
|
300
|
-
params:
|
|
312
|
+
params: GoalControlParamsT,
|
|
301
313
|
signal: AbortSignal | undefined,
|
|
302
314
|
_onUpdate: unknown,
|
|
303
315
|
ctx: ExtensionContext,
|
|
@@ -317,13 +329,14 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
317
329
|
details = handleReportBlocked(params, session, ports);
|
|
318
330
|
}
|
|
319
331
|
|
|
320
|
-
//
|
|
332
|
+
// 扁平化后 params.objective/reason 为 string|undefined(schema 不再按 action 收窄);
|
|
333
|
+
// handler 已对必填字段做存在性校验,此处用 ?. 兜底,未定义时空串
|
|
321
334
|
const text =
|
|
322
335
|
params.action === "create"
|
|
323
|
-
? `Goal created.\nGoal ID: ${details.goalId}\nSlug: ${details.slug ?? ""}\nObjective: ${params.objective
|
|
336
|
+
? `Goal created.\nGoal ID: ${details.goalId}\nSlug: ${details.slug ?? ""}\nObjective: ${params.objective?.trim() ?? ""}`
|
|
324
337
|
: params.action === "complete"
|
|
325
338
|
? `Goal completed.\nGoal ID: ${details.goalId}`
|
|
326
|
-
: `Goal reported blocked.\nGoal ID: ${details.goalId}\nReason: ${params.reason
|
|
339
|
+
: `Goal reported blocked.\nGoal ID: ${details.goalId}\nReason: ${params.reason?.trim() ?? ""}`;
|
|
327
340
|
|
|
328
341
|
// RPC 模式下附加 __gui__(用展开避免 details 来自 frozen 对象时加字段失败)
|
|
329
342
|
if (ctx.mode === "rpc" && session.state) {
|