@zhushanwen/pi-goal 0.11.0 → 0.12.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/README.md +28 -19
- package/package.json +7 -7
- package/src/__tests__/event-adapter.test.ts +1 -1
- package/src/__tests__/goal-control-adapter.test.ts +1 -1
- package/src/__tests__/goal-control-prompt.test.ts +4 -4
- package/src/__tests__/index.test.ts +3 -3
- package/src/__tests__/session.test.ts +1 -1
- package/src/adapters/command-adapter.ts +4 -21
- package/src/adapters/event-adapter.ts +1 -1
- package/src/adapters/event-handlers/agent-end.ts +1 -1
- package/src/adapters/event-handlers/before-agent-start.ts +1 -1
- package/src/adapters/goal-control-adapter.ts +5 -26
- package/src/adapters/ports.ts +9 -3
- package/src/adapters/success-criteria.ts +71 -0
- package/src/constants.ts +0 -5
- package/src/ports.ts +4 -1
- package/src/projection/__tests__/prompts.test.ts +23 -23
- package/src/projection/__tests__/widget.test.ts +2 -2
- package/src/projection/gui.ts +1 -1
- package/src/projection/prompts.ts +5 -12
- package/src/projection/widget.ts +1 -1
- package/src/service.ts +1 -15
- package/src/session.ts +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
# goal
|
|
2
2
|
|
|
3
|
-
Codex 风格的 `/goal` 命令 —
|
|
3
|
+
Codex 风格的 `/goal` 命令 — 持久目标追踪:`goal_control` 工具创建/完成/阻塞目标,完成需逐条对照 successCriteria 的证据验证,仅 token 维度预算。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- **持久化**:状态通过 session entries
|
|
7
|
+
- **目标管理**:`goal_control` tool 是唯一创建入口(create / complete / report_blocked)。`/goal <objective>` 是提示词触发器——不直接启动,而是引导 AI 调 `goal_control create`(slug / objective / successCriteria 均由 AI 在 toolcall 时决定)
|
|
8
|
+
- **证据验证**:complete 必须提供具体 evidence(改动的文件、通过的测试、运行的命令),且满足 create 时定义的每条 successCriteria 条件,不能空口完成
|
|
9
|
+
- **预算控制**:仅 token 维度预算(time budget 已移除,耗时仅记账显示)。70% 预警、90% 发收尾 steering、100% 且已发收尾提示后进入 budget_limited 终态。默认不设预算,仅在用户显式要求时设置
|
|
10
|
+
- **阻塞处理**:无自动检测——blocked 只能由 agent 主动调 `goal_control report_blocked`(穷尽替代方案后);用户经 `/goal resume` 恢复(paused/blocked → active)
|
|
11
|
+
- **持久化**:状态通过 session entries 保存(append-only),重启后从最新一条 goal-state entry 恢复,崩溃后保持原状态
|
|
12
|
+
|
|
13
|
+
## 状态机
|
|
14
|
+
|
|
15
|
+
6 态:`active` / `paused` / `blocked` / `complete` / `budget_limited` / `cancelled`。
|
|
16
|
+
`complete` / `budget_limited` / `cancelled` 为终态(不可逆)。
|
|
12
17
|
|
|
13
18
|
## 安装
|
|
14
19
|
|
|
15
20
|
```bash
|
|
16
21
|
# symlink 方式(开发推荐;globalExtDir 平铺布局,目标不带分组层)
|
|
17
|
-
ln -s /path/to/xyz-
|
|
22
|
+
ln -s /path/to/xyz-agent/extensions/universal/goal \
|
|
18
23
|
~/.pi/agent/extensions/goal
|
|
19
24
|
|
|
20
25
|
# npm 方式(正式)
|
|
@@ -25,26 +30,30 @@ pi install npm:@zhushanwen/pi-goal
|
|
|
25
30
|
|
|
26
31
|
```
|
|
27
32
|
/goal 修复项目中所有失败的测试
|
|
28
|
-
/goal 实现用户认证功能 --tokens 500000
|
|
29
|
-
/goal status #
|
|
30
|
-
/goal resume # 恢复(blocked → active)
|
|
31
|
-
/goal
|
|
33
|
+
/goal 实现用户认证功能 --tokens 500000
|
|
34
|
+
/goal status # 查看进度(criteria 逐条编号显示)
|
|
35
|
+
/goal resume # 恢复(paused/blocked → active)
|
|
36
|
+
/goal pause # 暂停(active → paused)
|
|
37
|
+
/goal update <new-objective> [--criteria <a;b;c>] # 重塑目标
|
|
38
|
+
/goal history # 查看历史 goal
|
|
39
|
+
/goal clear # 清除(→ cancelled)
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
## 文件结构
|
|
35
43
|
|
|
36
44
|
```
|
|
37
45
|
goal/
|
|
38
|
-
├── index.ts # 入口 —
|
|
46
|
+
├── index.ts # 入口 — re-export src/index.ts
|
|
39
47
|
└── src/
|
|
40
|
-
├── index.ts # 工厂入口(注册 tool/
|
|
41
|
-
├── commands.ts # /goal
|
|
48
|
+
├── index.ts # 工厂入口(注册 command/tool/events + __goalInit,全部委托 adapters)
|
|
49
|
+
├── commands.ts # /goal 命令参数解析(仅识别 --tokens flag)
|
|
42
50
|
├── constants.ts # 语义常量
|
|
43
51
|
├── ports.ts # Pi 能力抽象(Persistence/Ui/Messaging/Session)
|
|
44
|
-
├── service.ts # 协调层 —
|
|
52
|
+
├── service.ts # 协调层 — createGoal / finalizeAndPersist / applyEvent
|
|
45
53
|
├── persistence.ts # serialize/deserialize + GoalHistoryEntry
|
|
46
|
-
├── session.ts # 运行时句柄 +
|
|
47
|
-
├── engine/ # 零 Pi 依赖的纯状态机(goal/
|
|
48
|
-
├── adapters/ # Pi 桥接(
|
|
49
|
-
|
|
54
|
+
├── session.ts # 运行时句柄 + 状态重建(append-only 只读最新一条,无 entry GC)
|
|
55
|
+
├── engine/ # 零 Pi 依赖的纯状态机(goal/budget/types)
|
|
56
|
+
├── adapters/ # Pi 桥接(goal-control-adapter / command-adapter / event-adapter
|
|
57
|
+
│ # + event-handlers/ + ports / success-criteria)
|
|
58
|
+
└── projection/ # 渲染(widget / prompts / gui)
|
|
50
59
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
|
7
7
|
"xyz-agent": {
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"index.ts"
|
|
26
26
|
],
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
29
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
30
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
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
31
|
"typebox": "*",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.5.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.5.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@xyz-agent/extension-protocol": "0.
|
|
35
|
+
"@xyz-agent/extension-protocol": "0.8.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@earendil-works/pi-tui": {
|
|
@@ -110,7 +110,7 @@ function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeSta
|
|
|
110
110
|
// ── handleAgentEnd:ESC 守卫 ──────────────────────────
|
|
111
111
|
|
|
112
112
|
describe("handleAgentEnd — FR-6.7 ESC 守卫", () => {
|
|
113
|
-
it("aborted=true:不发 continuation
|
|
113
|
+
it("aborted=true:不发 continuation、不做 budget 检查、goal 保持 active", async () => {
|
|
114
114
|
const { pi, calls: piCalls, states } = makeFakePi();
|
|
115
115
|
const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
|
|
116
116
|
const session = createGoalSession();
|
|
@@ -118,7 +118,7 @@ describe("handleComplete — active 守卫 + evidence + finalizeAndPersist", ()
|
|
|
118
118
|
|
|
119
119
|
// ── handleCreate ─────────────────────────────────────
|
|
120
120
|
|
|
121
|
-
describe("handleCreate —
|
|
121
|
+
describe("handleCreate — objective/successCriteria 必填(handler 运行时校验)+ slug 真 optional + 非终态守卫 + createGoal", () => {
|
|
122
122
|
it("无旧 goal + slug + objective → status active + slug 写入 + 持久化 + notify", () => {
|
|
123
123
|
const session = createGoalSession();
|
|
124
124
|
const ports = makeFakePorts();
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// agent(LLM)唯一能看到的 tool 元信息就是 description + 报错文案。description 用中文
|
|
5
5
|
// 重写(动作 create/complete/report_blocked + 规则 + §2.5 终态语义),删除英文 Examples/Don't
|
|
6
|
-
// 段;runtime throw 保留 .trim() 空串校验 + 'Correct:'
|
|
7
|
-
//
|
|
6
|
+
// 段;runtime throw 保留 .trim() 空串校验 + 'Correct:' 纠正正例(schema 为扁平 Type.Object
|
|
7
|
+
// + action 字段级 union,字段全部 Optional——缺失必填由 handler 运行时校验兜底)。
|
|
8
8
|
//
|
|
9
9
|
// 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把中文语义/纠错文案/
|
|
10
10
|
// union 结构删掉。读源码而非 import,避免 mock 链(goal-control-adapter.ts 依赖
|
|
@@ -55,8 +55,8 @@ describe("goal_control description — 删除 Examples/Don't 英文段", () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
describe("goal_control schema —
|
|
59
|
-
it("Type.Union +
|
|
58
|
+
describe("goal_control schema — 扁平 Type.Object(OpenAI 兼容)+ handler 运行时校验", () => {
|
|
59
|
+
it("字段级 Type.Union + additionalProperties:false + 无 timeBudgetMinutes", () => {
|
|
60
60
|
expect(ADAPTER_SRC).toMatch(/Type\.Union\(/);
|
|
61
61
|
expect(ADAPTER_SRC).toContain("additionalProperties: false");
|
|
62
62
|
expect(ADAPTER_SRC).not.toMatch(/timeBudgetMinutes/);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* index.ts 测试 — 工厂入口 + 跨扩展 API(pi.__goalInit)
|
|
3
3
|
*
|
|
4
|
-
* 覆盖 T1.8 (NFR-AC-8):__goalInit 签名 (objective, budget, ctx)
|
|
5
|
-
* (task CRUD 已删除,D-16/FR-4 双轨消除)。active 守卫由 service.test.ts 覆盖。
|
|
4
|
+
* 覆盖 T1.8 (NFR-AC-8):__goalInit 签名 (objective, budget, ctx, slug?, successCriteria?),
|
|
5
|
+
* 结构上无法接收 tasks(task CRUD 已删除,D-16/FR-4 双轨消除)。active 守卫由 service.test.ts 覆盖。
|
|
6
6
|
*
|
|
7
7
|
* 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再调 pi.__goalInit。
|
|
8
8
|
*/
|
|
@@ -247,7 +247,7 @@ describe("E1 全链路集成:goal_control create 数组 → 落盘 → 重载
|
|
|
247
247
|
|
|
248
248
|
// 5. contextInjectionPrompt → <successCriteria> 段含编号行(plan 需求 3:编号列表)
|
|
249
249
|
const state = reloaded.state!;
|
|
250
|
-
const prompt = contextInjectionPrompt(state
|
|
250
|
+
const prompt = contextInjectionPrompt(state);
|
|
251
251
|
expect(prompt).toContain("<successCriteria>");
|
|
252
252
|
expect(prompt).toContain("1. e2e cond a");
|
|
253
253
|
expect(prompt).toContain("2. e2e cond b");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* session.ts 测试 — reconstructGoalState
|
|
3
3
|
*
|
|
4
4
|
* 覆盖:
|
|
5
|
-
* - MF-7: reconstructGoalState 3 条 FR(G-006
|
|
5
|
+
* - MF-7: reconstructGoalState 3 条 FR(G-006 append-only 不 splice / G-015 崩溃后非 active 保持原状 / G-024 throw→null)
|
|
6
6
|
*
|
|
7
7
|
* [REMOVED W4] isStaleContextError + STALE_CONTEXT_PATTERNS 测试随实现删除——
|
|
8
8
|
* 双重废(无生产调用方 + patterns 与 pi 0.84.1 真实 stale 文案零匹配),见
|
|
@@ -33,6 +33,7 @@ import type { GoalHistoryEntry } from "../ports";
|
|
|
33
33
|
import type { GoalSession } from "../session";
|
|
34
34
|
import { clearGoalSession } from "../session";
|
|
35
35
|
import { buildPorts } from "./ports";
|
|
36
|
+
import { CRITERIA_HINTS_SLASH, validateSuccessCriteriaItems } from "./success-criteria";
|
|
36
37
|
|
|
37
38
|
// ── Orchestrator ──────────────────────────────────────
|
|
38
39
|
|
|
@@ -282,29 +283,11 @@ function handleUpdate(
|
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
285
|
// 校验前置(mutate state 之前):任一条目非法 → throw,objective 等重塑字段全部不动。
|
|
285
|
-
//
|
|
286
|
+
// 逐条规则(typeof string / trim 非空 / 单行)与 goal_control create 共享
|
|
287
|
+
// validateSuccessCriteriaItems,slash 通道正例见 CRITERIA_HINTS_SLASH。
|
|
286
288
|
let validatedCriteria: string[] | undefined;
|
|
287
289
|
if (criteria !== undefined && criteria.length > 0) {
|
|
288
|
-
validatedCriteria =
|
|
289
|
-
for (const item of criteria) {
|
|
290
|
-
if (typeof item !== "string") {
|
|
291
|
-
throw new Error(
|
|
292
|
-
"'successCriteria' must be an array of strings. Correct: {\"action\":\"update\",\"objective\":\"...\",\"successCriteria\":[\"<condition 1>\",\"<condition 2>\"]}",
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
const trimmed = item.trim();
|
|
296
|
-
if (!trimmed) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
"'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: {\"action\":\"update\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\"]}",
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
if (/[\r\n]/.test(trimmed)) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
"'successCriteria' items must be single-line (no line breaks). Split multi-line text into separate semicolon-separated items, one condition per item. Correct: /goal update <objective> --criteria \"line 1; line 2\"",
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
validatedCriteria.push(trimmed);
|
|
307
|
-
}
|
|
290
|
+
validatedCriteria = validateSuccessCriteriaItems(criteria, CRITERIA_HINTS_SLASH);
|
|
308
291
|
}
|
|
309
292
|
const state = session.state;
|
|
310
293
|
const oldObjective = state.objective;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Event adapter — 薄路由(#4 拆分后)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 5 个事件 handler + 1 个共享辅助拆在 event-handlers/,本文件只做 re-export。
|
|
5
5
|
* handler 逻辑见各自文件。pi.on 注册在 index.ts(保持不变)。
|
|
6
6
|
*
|
|
7
7
|
* 行为等价(#4 验收):handler 逻辑原样搬迁,仅 persistAndUpdate 签名 ports 化
|
|
@@ -66,7 +66,7 @@ export async function handleBeforeAgentStart(
|
|
|
66
66
|
return {
|
|
67
67
|
message: {
|
|
68
68
|
customType: "goal-context",
|
|
69
|
-
content: contextInjectionPrompt(session.state
|
|
69
|
+
content: contextInjectionPrompt(session.state),
|
|
70
70
|
display: false,
|
|
71
71
|
},
|
|
72
72
|
};
|
|
@@ -42,8 +42,9 @@ import { updateWidget } from "../projection/widget";
|
|
|
42
42
|
import { createGoal, finalizeAndPersist, persistState, type ServicePorts, tickState } from "../service";
|
|
43
43
|
import type { GoalSession } from "../session";
|
|
44
44
|
import { buildPorts } from "./ports";
|
|
45
|
+
import { CRITERIA_HINTS_TOOL, validateSuccessCriteriaItems } from "./success-criteria";
|
|
45
46
|
|
|
46
|
-
// ── Params schema
|
|
47
|
+
// ── Params schema(扁平 Type.Object,OpenAI 兼容)────────────
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* goal_control 参数 schema。扁平 Type.Object + action 字段级 Type.Union(等价 enum)。
|
|
@@ -156,7 +157,8 @@ function requireCreateObjective(params: GoalControlParamsT): string {
|
|
|
156
157
|
|
|
157
158
|
/**
|
|
158
159
|
* create 的 successCriteria 校验:必填 + 非空数组 + 逐条 string/非空/单行,返回 trim 后数组。
|
|
159
|
-
*
|
|
160
|
+
* 逐条校验共享 validateSuccessCriteriaItems(与 /goal update 同规则),tool 通道正例
|
|
161
|
+
* 见 CRITERIA_HINTS_TOOL;校验顺序与错误消息不变。
|
|
160
162
|
*/
|
|
161
163
|
function requireCreateSuccessCriteria(params: GoalControlParamsT): string[] {
|
|
162
164
|
if (params.successCriteria === undefined) {
|
|
@@ -168,30 +170,7 @@ function requireCreateSuccessCriteria(params: GoalControlParamsT): string[] {
|
|
|
168
170
|
"'successCriteria' must be a non-empty array of 1~8 checkable conditions. Correct: {\"action\":\"create\",\"slug\":\"refactor-auth\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\",\"src/auth.ts uses JWT\"]}",
|
|
169
171
|
);
|
|
170
172
|
}
|
|
171
|
-
|
|
172
|
-
for (const item of successCriteriaRaw) {
|
|
173
|
-
// 运行时 typeof 防御(U28②):测试/直调可能绕过 schema 传脏元素,
|
|
174
|
-
// 此处给业务错误而非让下方 .trim() 抛 TypeError
|
|
175
|
-
if (typeof item !== "string") {
|
|
176
|
-
throw new Error(
|
|
177
|
-
"'successCriteria' must be an array of strings. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"<condition 1>\",\"<condition 2>\"]}",
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
const trimmed = item.trim();
|
|
181
|
-
if (!trimmed) {
|
|
182
|
-
throw new Error(
|
|
183
|
-
"'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"tests pass\",\"tsc clean\"]}",
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
// 换行校验(U7):/[\r\n]/ 覆盖 \n / \r\n / \r 三形态,schema pattern ^[^\r\n]+$ 的 handler 兜底
|
|
187
|
-
if (/[\r\n]/.test(trimmed)) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
"'successCriteria' items must be single-line (no line breaks). Split multi-line text into separate array items, one condition per item. Correct: {\"action\":\"create\",\"objective\":\"...\",\"successCriteria\":[\"line 1\",\"line 2\"]}",
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
successCriteria.push(trimmed);
|
|
193
|
-
}
|
|
194
|
-
return successCriteria;
|
|
173
|
+
return validateSuccessCriteriaItems(successCriteriaRaw, CRITERIA_HINTS_TOOL);
|
|
195
174
|
}
|
|
196
175
|
|
|
197
176
|
/** create 的 budget 解析:非法预算直接拒绝(不静默截断),未提供时返回空配置。 */
|
package/src/adapters/ports.ts
CHANGED
|
@@ -53,7 +53,12 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
|
|
|
53
53
|
// host 侧 event-adapter 检测 marker 解码还原。guiSetWidget 无 isGui 守卫
|
|
54
54
|
// (helpers.ts 仅查 ctx.ui?.setWidget 存在性),isGui 判定在 updateWidget
|
|
55
55
|
// 外层(projection/widget.ts)。
|
|
56
|
-
|
|
56
|
+
//
|
|
57
|
+
// 断言根因:pi ExtensionUIContext.custom 是泛型方法(返回 Promise<T>),
|
|
58
|
+
// 与 GuiContext.ui.custom 的具体返回类型静态不兼容,但 mode/hasUI/ui 其余
|
|
59
|
+
// 成员形状一致(ExtensionMode 与 GuiContext.mode union 完全相同)——
|
|
60
|
+
// 单层直接断言可过 tsc(guiSetWidget 仅读 ui.setWidget,custom 不参与)。
|
|
61
|
+
guiSetWidget(ctx as GuiContext, name, result);
|
|
57
62
|
},
|
|
58
63
|
setStatus(name: string, text: string | undefined): void {
|
|
59
64
|
ctx.ui.setStatus(name, text);
|
|
@@ -65,8 +70,9 @@ export function buildPorts(pi: ExtensionAPI, ctx: ExtensionContext): ServicePort
|
|
|
65
70
|
return Boolean(ctx.hasUI);
|
|
66
71
|
},
|
|
67
72
|
get isGui(): boolean {
|
|
68
|
-
// RPC 模式 = GUI 渲染通道有效;TUI/json/print 走 pi
|
|
69
|
-
|
|
73
|
+
// RPC 模式 = GUI 渲染通道有效;TUI/json/print 走 pi 原生渲染。
|
|
74
|
+
// 断言同 setGuiWidget:custom 泛型签名静态不兼容,mode 单字段运行时可靠。
|
|
75
|
+
return isGuiCapable(ctx as GuiContext);
|
|
70
76
|
},
|
|
71
77
|
// ThemeLike 形状:透传 ctx.ui.theme 的 fg/bold。
|
|
72
78
|
// Theme.fg 只接受 ThemeColor 字面量 union(SDK 契约);projection 层保证传入 union 内字面量,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* successCriteria 逐条校验(adapters 层双通道共享,DRY):
|
|
3
|
+
* - slash 通道:/goal update --criteria(command-adapter.ts handleUpdate)
|
|
4
|
+
* - tool 通道:goal_control create(goal-control-adapter.ts requireCreateSuccessCriteria)
|
|
5
|
+
*
|
|
6
|
+
* 三条校验规则(typeof string / trim 非空 / 单行)与错误消息前半句在两通道逐字一致;
|
|
7
|
+
* 「Correct:」恢复正例经 CriteriaUsageHints 参数化——notString/empty 两通道都是 JSON 形式
|
|
8
|
+
* (action 字段区分 update/create),仅 multiline 的 Correct 正例随通道不同
|
|
9
|
+
* (slash 用法提示 vs tool JSON 正例)。校验顺序与错误文案与折叠前完全一致。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 各通道的恢复正例(「Correct:」之后的部分) */
|
|
13
|
+
export interface CriteriaUsageHints {
|
|
14
|
+
/** 非 string 元素错误的 Correct: 正例 */
|
|
15
|
+
notString: string;
|
|
16
|
+
/** 空串项错误的 Correct: 正例 */
|
|
17
|
+
empty: string;
|
|
18
|
+
/** 单行校验错误的 Split 说明 + Correct: 正例 */
|
|
19
|
+
multiline: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** slash 通道(/goal update)正例 */
|
|
23
|
+
export const CRITERIA_HINTS_SLASH: CriteriaUsageHints = {
|
|
24
|
+
notString: '{"action":"update","objective":"...","successCriteria":["<condition 1>","<condition 2>"]}',
|
|
25
|
+
empty: '{"action":"update","objective":"...","successCriteria":["tests pass","tsc clean"]}',
|
|
26
|
+
multiline:
|
|
27
|
+
'Split multi-line text into separate semicolon-separated items, one condition per item. Correct: /goal update <objective> --criteria "line 1; line 2"',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** tool 通道(goal_control create)正例 */
|
|
31
|
+
export const CRITERIA_HINTS_TOOL: CriteriaUsageHints = {
|
|
32
|
+
notString: '{"action":"create","objective":"...","successCriteria":["<condition 1>","<condition 2>"]}',
|
|
33
|
+
empty: '{"action":"create","objective":"...","successCriteria":["tests pass","tsc clean"]}',
|
|
34
|
+
multiline:
|
|
35
|
+
'Split multi-line text into separate array items, one condition per item. Correct: {"action":"create","objective":"...","successCriteria":["line 1","line 2"]}',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 逐条校验并归一(trim),任一条非法即 throw(校验前置语义由调用方保证:
|
|
40
|
+
* mutate state 之前调用,任一条目非法时 state 不动)。
|
|
41
|
+
*
|
|
42
|
+
* 运行时 typeof 防御(U28②):测试/直调可能绕过 schema 传脏元素,
|
|
43
|
+
* 此处给业务错误而非让下方 .trim() 抛 TypeError。
|
|
44
|
+
* 换行校验(U7):/[\r\n]/ 覆盖 \n / \r\n / \r 三形态,schema pattern ^[^\r\n]+$ 的 handler 兜底。
|
|
45
|
+
*/
|
|
46
|
+
export function validateSuccessCriteriaItems(
|
|
47
|
+
items: readonly unknown[],
|
|
48
|
+
hints: CriteriaUsageHints,
|
|
49
|
+
): string[] {
|
|
50
|
+
const validated: string[] = [];
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (typeof item !== "string") {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`'successCriteria' must be an array of strings. Correct: ${hints.notString}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const trimmed = item.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`'successCriteria' items must not be empty. Each condition should be a short, single-line, checkable statement. Correct: ${hints.empty}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (/[\r\n]/.test(trimmed)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`'successCriteria' items must be single-line (no line breaks). ${hints.multiline}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
validated.push(trimmed);
|
|
69
|
+
}
|
|
70
|
+
return validated;
|
|
71
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -15,11 +15,6 @@ export const BUDGET_RATIO_HIGH = 0.9; // 90% — 触发预警/收尾
|
|
|
15
15
|
export const BUDGET_RATIO_LOW = 0.7; // 70% — 触发提醒
|
|
16
16
|
export const CONTEXT_USAGE_RATIO_LIMIT = 0.85; // 85% — 上下文空间不足阈值
|
|
17
17
|
|
|
18
|
-
// ── 预算百分比阈值 (0-100) ──────────────────────────
|
|
19
|
-
|
|
20
|
-
export const BUDGET_PERCENT_HIGH = 90; // widget 颜色变红
|
|
21
|
-
export const BUDGET_PERCENT_LOW = 70; // widget 颜色变黄
|
|
22
|
-
|
|
23
18
|
// ── 长度/数量上限 ───────────────────────────────────
|
|
24
19
|
|
|
25
20
|
export const UPDATE_PREFIX_LENGTH = 7; // "update ".length
|
package/src/ports.ts
CHANGED
|
@@ -26,7 +26,10 @@ export interface GoalHistoryEntry {
|
|
|
26
26
|
// ── PersistencePort ──────────────────────────────────
|
|
27
27
|
|
|
28
28
|
export interface PersistencePort {
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* 写入 goal-state entry。无 GC:session entries append-only,每次追加完整 snapshot,
|
|
31
|
+
* 不轮转、不删旧 entry(重建时只读最新一条,旧 entry 留存)。
|
|
32
|
+
*/
|
|
30
33
|
appendState(state: GoalRuntimeState): void;
|
|
31
34
|
/** 写入 goal-history 归档 entry */
|
|
32
35
|
appendHistory(entry: GoalHistoryEntry): void;
|
|
@@ -40,13 +40,13 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
|
|
|
40
40
|
budget: { tokenBudget: 1000 },
|
|
41
41
|
tokensUsed: 500,
|
|
42
42
|
});
|
|
43
|
-
const out = formatBudget(state,
|
|
43
|
+
const out = formatBudget(state, "percent");
|
|
44
44
|
expect(out).toContain("Token: 50%");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it("percent: 无预算 → 空字符串", () => {
|
|
48
48
|
const state = makeState();
|
|
49
|
-
expect(formatBudget(state,
|
|
49
|
+
expect(formatBudget(state, "percent")).toBe("");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it("line: 剩余/总量格式", () => {
|
|
@@ -54,7 +54,7 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
|
|
|
54
54
|
budget: { tokenBudget: 1000 },
|
|
55
55
|
tokensUsed: 300,
|
|
56
56
|
});
|
|
57
|
-
const out = formatBudget(state,
|
|
57
|
+
const out = formatBudget(state, "line");
|
|
58
58
|
expect(out).toContain("Tokens: 700/1000");
|
|
59
59
|
});
|
|
60
60
|
});
|
|
@@ -64,7 +64,7 @@ describe("formatBudget — 2 styles (FR-3.4)", () => {
|
|
|
64
64
|
describe("XML escaping in prompts", () => {
|
|
65
65
|
it("objective 中的 <>& 被转义(continuationPrompt)", () => {
|
|
66
66
|
const state = makeState({ objective: "<script>alert('x')</script> & data" });
|
|
67
|
-
const out = continuationPrompt(state
|
|
67
|
+
const out = continuationPrompt(state);
|
|
68
68
|
expect(out).toContain("<script>");
|
|
69
69
|
expect(out).toContain("& data");
|
|
70
70
|
expect(out).not.toContain("<script>");
|
|
@@ -90,7 +90,7 @@ describe("XML escaping in prompts", () => {
|
|
|
90
90
|
describe("continuationPrompt", () => {
|
|
91
91
|
it("含 objective + Turn + Completion audit 段落", () => {
|
|
92
92
|
const state = makeState({ currentTurnIndex: 3 });
|
|
93
|
-
const out = continuationPrompt(state
|
|
93
|
+
const out = continuationPrompt(state);
|
|
94
94
|
expect(out).toContain("Turn 3");
|
|
95
95
|
expect(out).toContain("test objective");
|
|
96
96
|
expect(out).toContain("Completion audit");
|
|
@@ -98,7 +98,7 @@ describe("continuationPrompt", () => {
|
|
|
98
98
|
|
|
99
99
|
it("对标 Codex 三约束:Completion audit / Fidelity / Blocked 完整", () => {
|
|
100
100
|
const state = makeState();
|
|
101
|
-
const out = continuationPrompt(state
|
|
101
|
+
const out = continuationPrompt(state);
|
|
102
102
|
// Completion audit: 逐项证据验证
|
|
103
103
|
expect(out).toContain("Completion audit");
|
|
104
104
|
expect(out).toContain("Evidence must prove completion");
|
|
@@ -115,14 +115,14 @@ describe("continuationPrompt", () => {
|
|
|
115
115
|
|
|
116
116
|
it("软建议 complete 前完成所有 todo(FR-6,全解耦后非强制)", () => {
|
|
117
117
|
const state = makeState();
|
|
118
|
-
const out = continuationPrompt(state
|
|
118
|
+
const out = continuationPrompt(state);
|
|
119
119
|
expect(out).toContain("Recommend finishing all todos");
|
|
120
120
|
expect(out).toContain("verification todos");
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it("含 plan audit 软提醒(FR-7/D27 prompt 驱动)", () => {
|
|
124
124
|
const state = makeState();
|
|
125
|
-
const out = continuationPrompt(state
|
|
125
|
+
const out = continuationPrompt(state);
|
|
126
126
|
expect(out).toContain("plan.md step was executed");
|
|
127
127
|
});
|
|
128
128
|
});
|
|
@@ -165,7 +165,7 @@ describe("contextInjectionPrompt", () => {
|
|
|
165
165
|
budget: { tokenBudget: 1000 },
|
|
166
166
|
tokensUsed: 200,
|
|
167
167
|
});
|
|
168
|
-
const out = contextInjectionPrompt(state
|
|
168
|
+
const out = contextInjectionPrompt(state);
|
|
169
169
|
expect(out).toContain("GOAL mode activated");
|
|
170
170
|
expect(out).toContain("Status: active");
|
|
171
171
|
expect(out).toContain("Turn: 2");
|
|
@@ -186,13 +186,13 @@ describe("contextInjectionPrompt", () => {
|
|
|
186
186
|
budget: { tokenBudget: 1000 },
|
|
187
187
|
tokensUsed: 200,
|
|
188
188
|
});
|
|
189
|
-
const out = contextInjectionPrompt(state
|
|
189
|
+
const out = contextInjectionPrompt(state);
|
|
190
190
|
expect(out.length).toBeLessThanOrEqual(600);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
it("删除冗余段(TC3):不含 todo 引导/plan 提示/Fidelity/Audit", () => {
|
|
194
194
|
const state = makeState();
|
|
195
|
-
const out = contextInjectionPrompt(state
|
|
195
|
+
const out = contextInjectionPrompt(state);
|
|
196
196
|
// todo 引导段已删(收敛到 continuation 软建议)
|
|
197
197
|
expect(out).not.toContain("Track work with todos");
|
|
198
198
|
expect(out).not.toContain("todo tool");
|
|
@@ -206,17 +206,17 @@ describe("contextInjectionPrompt", () => {
|
|
|
206
206
|
|
|
207
207
|
it("无 goal_manager 引用(#1 清理后)", () => {
|
|
208
208
|
const state = makeState();
|
|
209
|
-
const out = contextInjectionPrompt(state
|
|
209
|
+
const out = contextInjectionPrompt(state);
|
|
210
210
|
expect(out).not.toContain("goal_manager");
|
|
211
211
|
expect(out).not.toContain("create_tasks");
|
|
212
212
|
expect(out).not.toContain("add_subtasks");
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
it("无 planAvailable 参数(TC4 签名精简:仅 state
|
|
216
|
-
// planAvailable 恒 true 死分支已删;签名仅 (state
|
|
215
|
+
it("无 planAvailable 参数(TC4 签名精简:仅 state)", () => {
|
|
216
|
+
// planAvailable 恒 true 死分支已删;签名仅 (state)
|
|
217
217
|
const state = makeState();
|
|
218
|
-
expect(() => contextInjectionPrompt(state
|
|
219
|
-
const out = contextInjectionPrompt(state
|
|
218
|
+
expect(() => contextInjectionPrompt(state)).not.toThrow();
|
|
219
|
+
const out = contextInjectionPrompt(state);
|
|
220
220
|
expect(out).toContain("[GOAL mode activated]");
|
|
221
221
|
});
|
|
222
222
|
});
|
|
@@ -226,7 +226,7 @@ describe("contextInjectionPrompt", () => {
|
|
|
226
226
|
describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", () => {
|
|
227
227
|
it("continuationPrompt:有 successCriteria → 含 <successCriteria> 段 + 条件文案", () => {
|
|
228
228
|
const state = makeState({ successCriteria: ["pnpm test passes", "tsc clean"] });
|
|
229
|
-
const out = continuationPrompt(state
|
|
229
|
+
const out = continuationPrompt(state);
|
|
230
230
|
expect(out).toContain("<successCriteria>");
|
|
231
231
|
expect(out).toContain("</successCriteria>");
|
|
232
232
|
expect(out).toContain("1. pnpm test passes");
|
|
@@ -249,7 +249,7 @@ describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", ()
|
|
|
249
249
|
|
|
250
250
|
it("contextInjectionPrompt:有 successCriteria → 含 <successCriteria> 段 + 条件文案", () => {
|
|
251
251
|
const state = makeState({ successCriteria: ["file X exists"] });
|
|
252
|
-
const out = contextInjectionPrompt(state
|
|
252
|
+
const out = contextInjectionPrompt(state);
|
|
253
253
|
expect(out).toContain("<successCriteria>");
|
|
254
254
|
expect(out).toContain("file X exists");
|
|
255
255
|
// 条件文案(contextInjectionPrompt 专属)
|
|
@@ -266,24 +266,24 @@ describe("successCriteria 注入(<successCriteria> 段 + 条件文案)", ()
|
|
|
266
266
|
|
|
267
267
|
it("无 successCriteria → 不含 <successCriteria> 段(锁定 fallback)", () => {
|
|
268
268
|
const state = makeState(); // createGoalState 不带 successCriteria
|
|
269
|
-
const out = continuationPrompt(state
|
|
269
|
+
const out = continuationPrompt(state);
|
|
270
270
|
expect(out).not.toContain("<successCriteria>");
|
|
271
271
|
expect(out).not.toContain("every condition there must be met");
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
it("U17: successCriteria 空数组 → 与 undefined 同为空态,prompt 不增 <successCriteria> 段", () => {
|
|
275
275
|
const state = makeState({ successCriteria: [] });
|
|
276
|
-
expect(continuationPrompt(state
|
|
276
|
+
expect(continuationPrompt(state)).not.toContain("<successCriteria>");
|
|
277
277
|
expect(budgetLimitPrompt(state)).not.toContain("<successCriteria>");
|
|
278
278
|
expect(objectiveUpdatedPrompt(state, "old obj")).not.toContain("<successCriteria>");
|
|
279
|
-
expect(contextInjectionPrompt(state
|
|
279
|
+
expect(contextInjectionPrompt(state)).not.toContain("<successCriteria>");
|
|
280
280
|
// 条件文案同样不出现(continuationPrompt 专属分支)
|
|
281
|
-
expect(continuationPrompt(state
|
|
281
|
+
expect(continuationPrompt(state)).not.toContain("every condition there must be met");
|
|
282
282
|
});
|
|
283
283
|
|
|
284
284
|
it("successCriteria 中的 <>& 被转义(防注入)", () => {
|
|
285
285
|
const state = makeState({ successCriteria: ["<x> & y"] });
|
|
286
|
-
const out = continuationPrompt(state
|
|
286
|
+
const out = continuationPrompt(state);
|
|
287
287
|
expect(out).toContain("<x>");
|
|
288
288
|
expect(out).toContain("& y");
|
|
289
289
|
expect(out).not.toContain("<x>");
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 覆盖:
|
|
5
5
|
* - toSingleLine: 多行压缩
|
|
6
|
-
* - renderStatusLine: 状态后缀 + cancelled 短路 +
|
|
7
|
-
* - renderWidgetLines: cancelled 短路 +
|
|
6
|
+
* - renderStatusLine: slug 标题(fallback objective 截断)+ 状态后缀 + cancelled 短路 + token/耗时显示
|
|
7
|
+
* - renderWidgetLines: cancelled 短路 + 标题行 + criteria 摘要单行 + 预算进度条(Objective 全文行已移除,GAP-8)
|
|
8
8
|
* - renderTerminalStatusLine: 终态单行 + cancelled 短路
|
|
9
9
|
* - updateWidget: FR-6.6 hasUI 守卫 + 终态折叠 + cancelled 清除
|
|
10
10
|
*
|
package/src/projection/gui.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { formatTokens } from "./widget";
|
|
|
37
37
|
* 维度说明:status→severity(本函数)与 percent→color(widget.getBudgetColor)输入输出
|
|
38
38
|
* 双维度不同,不合并——本函数只管「状态语义」的严重度。
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
function goalStatusSeverity(status: GoalStatus): "ok" | "warn" | "danger" {
|
|
41
41
|
switch (status) {
|
|
42
42
|
case "active":
|
|
43
43
|
case "complete":
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Steering prompt 模板(projection 层)
|
|
3
3
|
*
|
|
4
|
-
* 类型 import 自 engine/types.ts
|
|
5
|
-
* timeUsedSeconds,不调 Date.now())。FR-3.4:formatBudget 单一出口。
|
|
4
|
+
* 类型 import 自 engine/types.ts。FR-3.4:formatBudget 单一出口。
|
|
6
5
|
*
|
|
7
6
|
* 设计原则(来自 Codex 调研):
|
|
8
7
|
* - Completion audit: 逐项证据验证,intent/partial progress 不是 evidence
|
|
@@ -55,15 +54,12 @@ export type BudgetFormatStyle = "percent" | "line";
|
|
|
55
54
|
* - "line" → ` | Tokens: remaining/total`(continuationPrompt 用)
|
|
56
55
|
*
|
|
57
56
|
* @param state runtime state(读 budget / tokensUsed)
|
|
58
|
-
* @param timeUsedSeconds 累计耗时秒数(保留位,FR-3.4 统一签名;percent/line 当前不消费)
|
|
59
57
|
* @param style 输出形式
|
|
60
58
|
*/
|
|
61
59
|
export function formatBudget(
|
|
62
60
|
state: GoalRuntimeState,
|
|
63
|
-
timeUsedSeconds: number,
|
|
64
61
|
style: BudgetFormatStyle,
|
|
65
62
|
): string {
|
|
66
|
-
void timeUsedSeconds; // FR-3.4 统一签名占位(percent/line 不消费,保留以便未来样式扩展)
|
|
67
63
|
if (style === "percent") {
|
|
68
64
|
return formatBudgetPercent(state);
|
|
69
65
|
}
|
|
@@ -88,9 +84,9 @@ function formatBudgetLine(state: GoalRuntimeState): string {
|
|
|
88
84
|
|
|
89
85
|
// ── Continuation Prompt ───────────────────────────────
|
|
90
86
|
|
|
91
|
-
export function continuationPrompt(state: GoalRuntimeState
|
|
87
|
+
export function continuationPrompt(state: GoalRuntimeState): string {
|
|
92
88
|
const objective = escapeXmlText(state.objective);
|
|
93
|
-
const budgetLine = formatBudget(state,
|
|
89
|
+
const budgetLine = formatBudget(state, "line");
|
|
94
90
|
const criteria = successCriteriaBlock(state);
|
|
95
91
|
|
|
96
92
|
return (
|
|
@@ -192,12 +188,9 @@ export function objectiveUpdatedPrompt(
|
|
|
192
188
|
* turn 末详述),避免两 prompt 重复。
|
|
193
189
|
* 删 planAvailable 参数(恒 true 死分支;plan 引导收敛到 continuation)。
|
|
194
190
|
*/
|
|
195
|
-
export function contextInjectionPrompt(
|
|
196
|
-
state: GoalRuntimeState,
|
|
197
|
-
timeUsedSeconds: number,
|
|
198
|
-
): string {
|
|
191
|
+
export function contextInjectionPrompt(state: GoalRuntimeState): string {
|
|
199
192
|
const objective = escapeXmlText(state.objective);
|
|
200
|
-
const budgetInfo = formatBudget(state,
|
|
193
|
+
const budgetInfo = formatBudget(state, "percent");
|
|
201
194
|
const criteria = successCriteriaBlock(state);
|
|
202
195
|
|
|
203
196
|
return (
|
package/src/projection/widget.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function formatTokens(n: number): string {
|
|
|
67
67
|
/**
|
|
68
68
|
* 返回 widget 标题:优先 slug,无 slug fallback objective 截断(单行)。
|
|
69
69
|
*/
|
|
70
|
-
|
|
70
|
+
function getTitle(state: GoalRuntimeState): string {
|
|
71
71
|
if (state.slug) return state.slug;
|
|
72
72
|
const objSingleLine = toSingleLine(state.objective);
|
|
73
73
|
return objSingleLine.length > OBJECTIVE_DISPLAY_LIMIT
|
package/src/service.ts
CHANGED
|
@@ -12,13 +12,7 @@
|
|
|
12
12
|
* FR-6.5: persist 前调 tick 累计时间
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
accumulateTokens,
|
|
17
|
-
checkBudgetOnResume,
|
|
18
|
-
checkBudgetOnTurnEnd,
|
|
19
|
-
tick,
|
|
20
|
-
} from "./engine/budget";
|
|
21
|
-
import type { BudgetDimension } from "./engine/budget";
|
|
15
|
+
import { accumulateTokens, checkBudgetOnTurnEnd, tick } from "./engine/budget";
|
|
22
16
|
import { createGoalState, isActiveStatus, transitionStatus } from "./engine/goal";
|
|
23
17
|
import type { BudgetConfig, GoalRuntimeState, GoalStatus } from "./engine/types";
|
|
24
18
|
import { makeHistoryEntry, serializeState } from "./persistence";
|
|
@@ -277,11 +271,3 @@ export function applyEvent(
|
|
|
277
271
|
break;
|
|
278
272
|
}
|
|
279
273
|
}
|
|
280
|
-
|
|
281
|
-
// ── resume 预算重检(供 command-adapter 调用)─────────
|
|
282
|
-
|
|
283
|
-
export function checkResumeBudget(
|
|
284
|
-
state: GoalRuntimeState,
|
|
285
|
-
): { type: "exceeded"; dimension: BudgetDimension } | null {
|
|
286
|
-
return checkBudgetOnResume(state);
|
|
287
|
-
}
|
package/src/session.ts
CHANGED