@zhushanwen/pi-goal 0.1.0 → 0.1.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/README.md +30 -132
- package/package.json +8 -2
- package/src/index.ts +16 -13
- package/src/tool-handler.ts +6 -6
package/README.md
CHANGED
|
@@ -1,151 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# goal
|
|
2
2
|
|
|
3
|
-
Codex 风格的 `/goal`
|
|
3
|
+
Codex 风格的 `/goal` 命令 — 持久目标驱动自主循环,支持任务追踪、证据验证、Token/时间预算、阻塞检测。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **自主循环**:`/goal <目标>` 启动后,AI 自动拆分任务并持续执行
|
|
8
|
+
- **证据验证**:完成任务必须提供具体证据,不能空口完成
|
|
9
|
+
- **预算控制**:Token 和时间双预算,70% 预警、90% 收尾、100% 终止
|
|
10
|
+
- **阻塞检测**:连续无进展自动阻塞,用户可手动 resume
|
|
11
|
+
- **持久化**:状态通过 session entries 保存,重启后自动恢复
|
|
4
12
|
|
|
5
13
|
## 安装
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
# symlink 方式(开发推荐)
|
|
17
|
+
ln -s /path/to/xyz-pi-extensions-workspace/main/packages/goal \
|
|
18
|
+
~/.pi/agent/extensions/goal
|
|
11
19
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
ln -s /path/to/xyz-pi-extensions/goal .pi/extensions/goal
|
|
20
|
+
# npm 方式(正式)
|
|
21
|
+
pi install npm:@zhushanwen/pi-goal
|
|
15
22
|
```
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## 快速开始
|
|
24
|
+
## 使用
|
|
20
25
|
|
|
21
26
|
```
|
|
22
27
|
/goal 修复项目中所有失败的测试
|
|
23
28
|
/goal 实现用户认证功能 --tokens 500000 --timeout 30
|
|
29
|
+
/goal status # 查看进度
|
|
30
|
+
/goal pause # 暂停
|
|
31
|
+
/goal resume # 恢复
|
|
32
|
+
/goal clear # 清除
|
|
24
33
|
```
|
|
25
34
|
|
|
26
|
-
Pi 会自动:拆分任务 → 逐个执行 → 收集证据 → 完成目标。
|
|
27
|
-
|
|
28
|
-
## 命令参考
|
|
29
|
-
|
|
30
|
-
| 命令 | 说明 |
|
|
31
|
-
|------|------|
|
|
32
|
-
| `/goal <目标>` | 设定新目标(会替换当前未完成的目标) |
|
|
33
|
-
| `/goal status` | 查看当前目标状态、进度、预算 |
|
|
34
|
-
| `/goal pause` | 暂停目标 |
|
|
35
|
-
| `/goal resume` | 恢复暂停/阻塞的目标 |
|
|
36
|
-
| `/goal clear` | 清除目标 |
|
|
37
|
-
| `/goal update <新目标>` | 更新目标描述(清除旧任务,重新规划) |
|
|
38
|
-
|
|
39
|
-
## 参数
|
|
40
|
-
|
|
41
|
-
| 参数 | 默认值 | 说明 |
|
|
42
|
-
|------|--------|------|
|
|
43
|
-
| `--tokens N` | 不限制 | Token 预算上限 |
|
|
44
|
-
| `--timeout N` | 不限制 | 时间预算(分钟) |
|
|
45
|
-
| `--max-turns N` | 50 | 最大轮次 |
|
|
46
|
-
| `--max-stall N` | 5 | 连续无进展轮次,超过后自动阻塞 |
|
|
47
|
-
|
|
48
|
-
示例:
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
/goal 重构 auth 模块 --tokens 200000 --timeout 15 --max-turns 30
|
|
52
|
-
/goal 写单元测试 --tokens 100000 --max-stall 3
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 工作流程
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
用户: /goal 修复登录bug
|
|
59
|
-
│
|
|
60
|
-
▼
|
|
61
|
-
Pi 注入上下文 → LLM 拆分任务 (create_tasks)
|
|
62
|
-
│
|
|
63
|
-
▼
|
|
64
|
-
┌─────────────────────────────┐
|
|
65
|
-
│ 每个 turn: │
|
|
66
|
-
│ 1. LLM 执行任务 │
|
|
67
|
-
│ 2. 完成后调用 complete_task │
|
|
68
|
-
│ (必须提供具体证据) │
|
|
69
|
-
│ 3. 检查预算/进展/阻塞 │
|
|
70
|
-
│ 4. 注入 continuation 继续 │
|
|
71
|
-
└─────────┬───────────────────┘
|
|
72
|
-
│ 全部完成
|
|
73
|
-
▼
|
|
74
|
-
LLM 调用 complete_goal (提供整体证据)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 目标状态
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
┌──────────┐
|
|
81
|
-
┌───►│ active │◄─── resume
|
|
82
|
-
│ └────┬─────┘
|
|
83
|
-
│ │
|
|
84
|
-
clear ┌──┴──┬──────────┬───────────┬──────────┐
|
|
85
|
-
│ ▼ ▼ ▼ ▼ ▼
|
|
86
|
-
│ paused blocked complete budget_limited time_limited
|
|
87
|
-
│ ▲ ▲
|
|
88
|
-
│ │ │
|
|
89
|
-
└────┴─────┘ (resume 恢复)
|
|
90
|
-
|
|
91
|
-
可恢复: paused, blocked → active
|
|
92
|
-
终态: complete, budget_limited, time_limited, cancelled
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## 内置保护机制
|
|
96
|
-
|
|
97
|
-
| 机制 | 说明 |
|
|
98
|
-
|------|------|
|
|
99
|
-
| 证据验证 | `complete_task` / `complete_goal` 必须提供具体证据,不能空口完成 |
|
|
100
|
-
| 预算预警 | Token/时间达 70% 提示注意,90% 注入收尾 steering |
|
|
101
|
-
| 预算终止 | Token 100% → budget_limited 终态 |
|
|
102
|
-
| 阻塞检测 | 连续 N 轮无进展 → blocked,需用户手动 resume |
|
|
103
|
-
| 去抖保护 | 本轮 token 消耗为 0 时不发 continuation,防止无限循环 |
|
|
104
|
-
| 防重入 | `before_agent_start` 注入的上下文不会在 `agent_end` 重复发送 |
|
|
105
|
-
| Resume 预算重检 | 恢复暂停目标时,如果预算已耗尽,直接拒绝 |
|
|
106
|
-
| 零预算拒绝 | `--tokens 0` 会被拒绝 |
|
|
107
|
-
| 空白目标拒绝 | `/goal ""` 或 `/goal " "` 会被拒绝 |
|
|
108
|
-
| 目标长度限制 | 超过 4000 字符会被拒绝 |
|
|
109
|
-
| 上下文空间保护 | 上下文窗口 >85% 时暂停,防止 OOM |
|
|
110
|
-
|
|
111
|
-
## goal_manager 工具
|
|
112
|
-
|
|
113
|
-
Pi 注册了 `goal_manager` 工具供 LLM 调用,用户不需要直接操作:
|
|
114
|
-
|
|
115
|
-
| Action | 说明 |
|
|
116
|
-
|--------|------|
|
|
117
|
-
| `create_tasks` | 拆分目标为任务清单 |
|
|
118
|
-
| `complete_task` | 标记任务完成(需 taskId + evidence) |
|
|
119
|
-
| `list_tasks` | 查看任务进度和剩余预算 |
|
|
120
|
-
| `complete_goal` | 标记目标完成(需 evidence,且所有任务已完成) |
|
|
121
|
-
| `cancel_goal` | 取消目标(用户要求退出/停止时,LLM 直接调用,一步退出) |
|
|
122
|
-
| `report_blocked` | 报告阻塞原因 |
|
|
123
|
-
|
|
124
|
-
每个工具响应都包含当前预算信息(已用/剩余),让 LLM 自我调节。
|
|
125
|
-
|
|
126
|
-
## 注意事项
|
|
127
|
-
|
|
128
|
-
- 目标描述会被 XML 转义,防止 prompt 注入
|
|
129
|
-
- `/goal update` 会清除已有任务和计数器,允许重新规划
|
|
130
|
-
- 已暂停的目标 resume 时会重新检查预算,超限则直接终止
|
|
131
|
-
- 状态通过 session entries 持久化,session 重启后自动恢复
|
|
132
|
-
- Token 统计排除 cached input,避免跨 turn 双重计算
|
|
133
|
-
- **退出方式有三种**:`/goal clear`(用户手动)、`/goal pause`(暂停稍后继续)、或直接告诉 LLM「停止/取消/退出」,LLM 会调用 `cancel_goal` 一步退出
|
|
134
|
-
|
|
135
35
|
## 文件结构
|
|
136
36
|
|
|
137
37
|
```
|
|
138
38
|
goal/
|
|
139
|
-
├── index.ts
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
39
|
+
├── index.ts
|
|
40
|
+
└── src/
|
|
41
|
+
├── index.ts # 入口 — 命令、事件、工具注册
|
|
42
|
+
├── state.ts # 状态机(6 态)
|
|
43
|
+
├── commands.ts # 命令参数解析
|
|
44
|
+
├── templates.ts # Steering prompt 模板
|
|
45
|
+
├── tool-handler.ts# goal_manager 工具处理
|
|
46
|
+
├── budget.ts # 预算计算
|
|
47
|
+
├── constants.ts # 常量
|
|
48
|
+
└── widget.ts # TUI 状态栏渲染
|
|
147
49
|
```
|
|
148
|
-
|
|
149
|
-
## License
|
|
150
|
-
|
|
151
|
-
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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
|
+
"type": "module",
|
|
5
6
|
"main": "src/index.ts",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./src/index.ts"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
6
12
|
"keywords": [
|
|
7
|
-
"pi",
|
|
13
|
+
"pi-package",
|
|
8
14
|
"extension",
|
|
9
15
|
"goal",
|
|
10
16
|
"autonomous",
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
* - deserializeState 向后兼容旧格式
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ExtensionAPI, ExtensionContext, CustomEntry } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, CustomEntry, Theme } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
|
|
20
21
|
import { Text } from "@mariozechner/pi-tui";
|
|
22
|
+
import { type Static } from "typebox";
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
import {
|
|
23
26
|
type GoalTask,
|
|
@@ -141,8 +144,8 @@ function reconstructGoalState(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
141
144
|
|
|
142
145
|
// ── Command Handler ───────────────────────────────────
|
|
143
146
|
|
|
144
|
-
async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: string, ctx: ExtensionContext): Promise<void> {
|
|
145
|
-
const parsed = parseGoalArgs(args);
|
|
147
|
+
async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: string | undefined, ctx: ExtensionContext): Promise<void> {
|
|
148
|
+
const parsed = parseGoalArgs(args ?? "");
|
|
146
149
|
|
|
147
150
|
switch (parsed.action) {
|
|
148
151
|
case "status": {
|
|
@@ -742,7 +745,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
742
745
|
],
|
|
743
746
|
parameters: GoalManagerParams,
|
|
744
747
|
|
|
745
|
-
async execute(_toolCallId, params
|
|
748
|
+
async execute(_toolCallId: string, params: Static<typeof GoalManagerParams>, _signal: AbortSignal | undefined, _onUpdate: any, ctx: ExtensionContext) {
|
|
746
749
|
try {
|
|
747
750
|
return await executeGoalAction(pi, session, params, ctx);
|
|
748
751
|
} catch (err) {
|
|
@@ -752,7 +755,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
752
755
|
}
|
|
753
756
|
},
|
|
754
757
|
|
|
755
|
-
renderCall(args, theme) {
|
|
758
|
+
renderCall(args: any, theme: Theme) {
|
|
756
759
|
let text = theme.fg("toolTitle", theme.bold("goal_manager ")) + theme.fg("muted", args.action);
|
|
757
760
|
if (args.tasks) text += ` ${theme.fg("dim", `(${args.tasks.length} tasks)`)}`;
|
|
758
761
|
if (args.updates) text += ` ${theme.fg("dim", `(${args.updates.length} updates)`)}`;
|
|
@@ -763,7 +766,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
763
766
|
return new Text(text, 0, 0);
|
|
764
767
|
},
|
|
765
768
|
|
|
766
|
-
renderResult(result, { expanded }, theme) {
|
|
769
|
+
renderResult(result: any, { expanded }: any, theme: Theme) {
|
|
767
770
|
const details = result.details as GoalManagerDetails | undefined;
|
|
768
771
|
if (!details || !Array.isArray(details.tasks)) {
|
|
769
772
|
const text = result.content[0];
|
|
@@ -811,14 +814,14 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
811
814
|
pi.registerCommand("goal", {
|
|
812
815
|
description:
|
|
813
816
|
"目标驱动模式: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
|
|
814
|
-
handler: async (args, ctx) => {
|
|
817
|
+
handler: async (args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
815
818
|
await handleGoalCommand(pi, session, args, ctx);
|
|
816
819
|
},
|
|
817
820
|
});
|
|
818
821
|
|
|
819
822
|
// ── Event: before_agent_start ──────────────────────
|
|
820
823
|
|
|
821
|
-
pi.on("before_agent_start", async (_event, ctx) => {
|
|
824
|
+
pi.on("before_agent_start", async (_event: any, ctx: ExtensionContext) => {
|
|
822
825
|
return handleBeforeAgentStart(pi, session, ctx);
|
|
823
826
|
});
|
|
824
827
|
|
|
@@ -831,7 +834,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
831
834
|
|
|
832
835
|
// ── Event: turn_end ────────────────────────────────
|
|
833
836
|
|
|
834
|
-
pi.on("turn_end", async (_event, ctx) => {
|
|
837
|
+
pi.on("turn_end", async (_event: any, ctx: ExtensionContext) => {
|
|
835
838
|
if (!session.state) return;
|
|
836
839
|
session.state.currentTurnIndex++;
|
|
837
840
|
updateWidget(session, ctx);
|
|
@@ -839,7 +842,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
839
842
|
|
|
840
843
|
// ── Event: message_end (token accounting) ──────────
|
|
841
844
|
|
|
842
|
-
pi.on("message_end", async (event, _ctx) => {
|
|
845
|
+
pi.on("message_end", async (event: any, _ctx: ExtensionContext) => {
|
|
843
846
|
if (!session.state || !isActiveStatus(session.state.status)) return;
|
|
844
847
|
if (event.message.role !== "assistant") return;
|
|
845
848
|
|
|
@@ -858,13 +861,13 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
858
861
|
|
|
859
862
|
// ── Event: agent_end ───────────────────────────────
|
|
860
863
|
|
|
861
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
864
|
+
pi.on("agent_end", async (_event: any, ctx: ExtensionContext) => {
|
|
862
865
|
await handleAgentEnd(pi, session, ctx);
|
|
863
866
|
});
|
|
864
867
|
|
|
865
868
|
// ── Event: session_start (state reconstruction) ───
|
|
866
869
|
|
|
867
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
870
|
+
pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
|
|
868
871
|
reconstructGoalState(pi, session, ctx);
|
|
869
872
|
if (session.state) {
|
|
870
873
|
session.tasksCompletedAtAgentStart = getCompletedCount(session.state.tasks);
|
|
@@ -881,7 +884,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
881
884
|
];
|
|
882
885
|
|
|
883
886
|
for (const customType of goalMessageTypes) {
|
|
884
|
-
pi.registerMessageRenderer(customType, (message, _options, theme) => {
|
|
887
|
+
pi.registerMessageRenderer(customType, (message: any, _options: any, theme: Theme) => {
|
|
885
888
|
const prefix =
|
|
886
889
|
message.customType === "goal-context-exceeded"
|
|
887
890
|
? theme.fg("error", "[GOAL 预算] ")
|
package/src/tool-handler.ts
CHANGED
|
@@ -237,7 +237,7 @@ export async function executeGoalAction(
|
|
|
237
237
|
`如需追加任务请用 add_tasks,如需全部重新规划请用 /goal update。`,
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
|
-
state.tasks = params.tasks.map((desc, i) => ({
|
|
240
|
+
state.tasks = params.tasks.map((desc: string, i: number) => ({
|
|
241
241
|
id: i + 1,
|
|
242
242
|
description: normalizeDescription(desc),
|
|
243
243
|
status: "pending" as const,
|
|
@@ -256,7 +256,7 @@ export async function executeGoalAction(
|
|
|
256
256
|
const startId = state.tasks.length > 0
|
|
257
257
|
? Math.max(...state.tasks.map((t) => t.id)) + 1
|
|
258
258
|
: 1;
|
|
259
|
-
const newTasks: GoalTask[] = params.tasks.map((desc, i) => ({
|
|
259
|
+
const newTasks: GoalTask[] = params.tasks.map((desc: string, i: number) => ({
|
|
260
260
|
id: startId + i,
|
|
261
261
|
description: normalizeDescription(desc),
|
|
262
262
|
status: "pending" as const,
|
|
@@ -273,8 +273,8 @@ export async function executeGoalAction(
|
|
|
273
273
|
if (!params.updates || params.updates.length === 0) {
|
|
274
274
|
throw new Error("update_tasks requires a non-empty updates array");
|
|
275
275
|
}
|
|
276
|
-
const taskIds = params.updates.map((u) => u.taskId);
|
|
277
|
-
const duplicateIds = taskIds.filter((id, i) => taskIds.indexOf(id) !== i);
|
|
276
|
+
const taskIds = params.updates.map((u: { taskId: number; status: string; evidence?: string }) => u.taskId);
|
|
277
|
+
const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
|
|
278
278
|
if (duplicateIds.length > 0) {
|
|
279
279
|
throw new Error(`重复的 taskId: ${[...new Set(duplicateIds)].join(", ")}`);
|
|
280
280
|
}
|
|
@@ -400,11 +400,11 @@ export async function executeGoalAction(
|
|
|
400
400
|
}
|
|
401
401
|
const subtasks = parentTask.subtasks ?? [];
|
|
402
402
|
const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
|
|
403
|
-
const trimmed = params.texts.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
403
|
+
const trimmed = params.texts.map((t: string) => t.trim()).filter((t: string) => t.length > 0);
|
|
404
404
|
if (trimmed.length === 0) {
|
|
405
405
|
throw new Error("texts 中至少需要一个非空字符串");
|
|
406
406
|
}
|
|
407
|
-
const newSubtasks: Subtask[] = trimmed.map((text, i) => ({
|
|
407
|
+
const newSubtasks: Subtask[] = trimmed.map((text: string, i: number) => ({
|
|
408
408
|
id: startId + i,
|
|
409
409
|
text,
|
|
410
410
|
status: "pending" as const,
|