@zhushanwen/pi-goal 0.1.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 ADDED
@@ -0,0 +1,151 @@
1
+ # /goal — Pi 目标驱动模式
2
+
3
+ Codex 风格的 `/goal` 命令,让 Pi 进入自主循环,持续工作直到目标达成。支持任务追踪、证据验证、Token/时间预算、阻塞检测。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 全局安装(所有项目生效)
9
+ git clone https://github.com/zhushanwen321/xyz-pi-extensions.git
10
+ ln -s $(pwd)/xyz-pi-extensions/goal ~/.pi/agent/extensions/goal
11
+
12
+ # 项目级安装
13
+ mkdir -p .pi/extensions
14
+ ln -s /path/to/xyz-pi-extensions/goal .pi/extensions/goal
15
+ ```
16
+
17
+ 安装后重启 Pi session 生效。
18
+
19
+ ## 快速开始
20
+
21
+ ```
22
+ /goal 修复项目中所有失败的测试
23
+ /goal 实现用户认证功能 --tokens 500000 --timeout 30
24
+ ```
25
+
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
+ ## 文件结构
136
+
137
+ ```
138
+ goal/
139
+ ├── index.ts # 入口 — 命令、事件、工具注册
140
+ ├── src/
141
+ │ ├── index.ts # Extension 主逻辑
142
+ │ ├── state.ts # 状态机、类型、序列化
143
+ │ ├── commands.ts # 命令参数解析
144
+ │ ├── templates.ts # Steering prompt 模板
145
+ │ └── widget.ts # TUI 状态栏渲染
146
+ └── package.json # 元数据(仅标识用)
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/index.ts";
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@zhushanwen/pi-goal",
3
+ "version": "0.1.0",
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
+ "main": "src/index.ts",
6
+ "keywords": [
7
+ "pi",
8
+ "extension",
9
+ "goal",
10
+ "autonomous",
11
+ "loop"
12
+ ],
13
+ "license": "MIT",
14
+ "files": [
15
+ "src/",
16
+ "index.ts"
17
+ ],
18
+ "peerDependencies": {
19
+ "@mariozechner/pi-coding-agent": "*"
20
+ },
21
+ "scripts": {
22
+ "typecheck": "npx tsc --noEmit"
23
+ }
24
+ }
package/src/budget.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Goal 预算策略 — 阈值、计算、检查
3
+ *
4
+ * 集中管理 token 和时间预算的所有决策逻辑。
5
+ * 调用者(handleAgentEnd、resume)通过 checkBudget() 获取决策结果,
6
+ * 不再需要了解阈值细节。
7
+ */
8
+
9
+ import type { GoalRuntimeState } from "./state";
10
+ import { getElapsedTimeSeconds } from "./state";
11
+ import {
12
+ SECONDS_PER_MINUTE,
13
+ PERCENT_FACTOR,
14
+ BUDGET_RATIO_HIGH,
15
+ BUDGET_RATIO_LOW,
16
+ BUDGET_RATIO_TIGHT,
17
+ BUDGET_PERCENT_HIGH,
18
+ BUDGET_PERCENT_LOW,
19
+ } from "./constants";
20
+
21
+ // ── 决策类型 ────────────────────────────────────────
22
+
23
+ export type BudgetDecision =
24
+ | { type: "ok" }
25
+ | { type: "warning70"; dimension: "token" | "time" }
26
+ | { type: "warning90"; dimension: "token" | "time" }
27
+ | { type: "steer_limit"; dimension: "token" | "time" }
28
+ | { type: "exceeded"; dimension: "token" | "time" };
29
+
30
+ // ── 百分比计算(供 widget 使用)──────────────────────
31
+
32
+ export function getTokenUsagePercent(state: GoalRuntimeState): number {
33
+ if (!state.budget.tokenBudget || state.budget.tokenBudget <= 0) return 0;
34
+ return (state.tokensUsed / state.budget.tokenBudget) * PERCENT_FACTOR;
35
+ }
36
+
37
+ export function getTimeUsagePercent(state: GoalRuntimeState): number {
38
+ if (!state.budget.timeBudgetMinutes || state.budget.timeBudgetMinutes <= 0) return 0;
39
+ const elapsed = getElapsedTimeSeconds(state);
40
+ const budgetSeconds = state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE;
41
+ return (elapsed / budgetSeconds) * PERCENT_FACTOR;
42
+ }
43
+
44
+ // ── Widget 颜色阈值(供 widget.ts 使用)──────────────
45
+
46
+ export function getBudgetColor(percent: number): "error" | "warning" | "muted" {
47
+ if (percent >= BUDGET_PERCENT_HIGH) return "error";
48
+ if (percent >= BUDGET_PERCENT_LOW) return "warning";
49
+ return "muted";
50
+ }
51
+
52
+ // ── Resume 时的预算检查 ──────────────────────────────
53
+
54
+ export function checkBudgetOnResume(state: GoalRuntimeState): { type: "exceeded"; dimension: "token" | "time" } | null {
55
+ if (state.budget.tokenBudget && state.tokensUsed >= state.budget.tokenBudget) {
56
+ return { type: "exceeded", dimension: "token" };
57
+ }
58
+ if (state.budget.timeBudgetMinutes) {
59
+ const elapsed = getElapsedTimeSeconds(state);
60
+ if (elapsed >= state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE) {
61
+ return { type: "exceeded", dimension: "time" };
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ // ── Agent turn 结束时的预算检查 ─────────────────────
68
+
69
+ export interface BudgetCheckResult {
70
+ /** 终止性决策(预算耗尽时只返回一个,优先 token) */
71
+ terminal: { type: "exceeded"; dimension: "token" | "time" } | null;
72
+ /** 需要发送的预警(70%/90%) */
73
+ warnings: BudgetDecision[];
74
+ /** 是否已发送过 steering(90% token 收尾) */
75
+ shouldSendSteering: boolean;
76
+ }
77
+
78
+ export function checkBudgetOnTurnEnd(state: GoalRuntimeState): BudgetCheckResult {
79
+ const result: BudgetCheckResult = {
80
+ terminal: null,
81
+ warnings: [],
82
+ shouldSendSteering: false,
83
+ };
84
+
85
+ // Token 预算检查
86
+ if (state.budget.tokenBudget) {
87
+ const tokenPct = state.tokensUsed / state.budget.tokenBudget;
88
+
89
+ // 100% 耗尽 + 已发过 steering → 终止
90
+ if (tokenPct >= 1 && state.budgetLimitSteeringSent) {
91
+ result.terminal = { type: "exceeded", dimension: "token" };
92
+ return result;
93
+ }
94
+
95
+ // 90% + 未发 steering → 发 steer
96
+ if (tokenPct >= BUDGET_RATIO_HIGH && !state.budgetLimitSteeringSent) {
97
+ result.shouldSendSteering = true;
98
+ return result;
99
+ }
100
+
101
+ // 90% 预警(未发过)
102
+ if (tokenPct >= BUDGET_RATIO_HIGH && !state.budgetWarning90Sent) {
103
+ result.warnings.push({ type: "warning90", dimension: "token" });
104
+ } else if (tokenPct >= BUDGET_RATIO_LOW && !state.budgetWarning70Sent) {
105
+ result.warnings.push({ type: "warning70", dimension: "token" });
106
+ }
107
+ }
108
+
109
+ // 时间预算检查
110
+ if (state.budget.timeBudgetMinutes) {
111
+ const elapsed = getElapsedTimeSeconds(state);
112
+ const timePct = elapsed / (state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE);
113
+
114
+ if (elapsed >= state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE) {
115
+ result.terminal = { type: "exceeded", dimension: "time" };
116
+ return result;
117
+ }
118
+
119
+ if (timePct >= BUDGET_RATIO_HIGH && !state.budgetWarning90Sent) {
120
+ result.warnings.push({ type: "warning90", dimension: "time" });
121
+ } else if (timePct >= BUDGET_RATIO_LOW && !state.budgetWarning70Sent) {
122
+ result.warnings.push({ type: "warning70", dimension: "time" });
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ // ── 进展评估 ────────────────────────────────────────
130
+
131
+ export interface ProgressCheck {
132
+ allTasksDone: boolean;
133
+ noTasksCreated: boolean;
134
+ maxTurnsReached: boolean;
135
+ isStalled: boolean;
136
+ budgetTight: boolean;
137
+ completedCount: number;
138
+ totalCount: number;
139
+ }
140
+
141
+ export function checkProgress(state: GoalRuntimeState, tasksCompletedAtStart: number): ProgressCheck {
142
+ const incomplete = state.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
143
+ const completedCount = state.tasks.filter((t) => t.status === "completed").length;
144
+ const totalCount = state.tasks.length;
145
+ const progressThisRound = completedCount - tasksCompletedAtStart;
146
+
147
+ return {
148
+ allTasksDone: totalCount > 0 && incomplete.length === 0 && completedCount > 0,
149
+ noTasksCreated: totalCount === 0,
150
+ maxTurnsReached: state.turnCount >= state.budget.maxTurns,
151
+ isStalled: progressThisRound === 0,
152
+ budgetTight: Boolean(
153
+ state.budget.tokenBudget &&
154
+ state.tokensUsed >= state.budget.tokenBudget * BUDGET_RATIO_TIGHT,
155
+ ),
156
+ completedCount,
157
+ totalCount,
158
+ };
159
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * /goal 命令定义和参数解析
3
+ */
4
+
5
+ import type { BudgetConfig } from "./state";
6
+ import { MAX_TURNS_CAP, MAX_STALL_CAP, UPDATE_PREFIX_LENGTH } from "./constants";
7
+
8
+ export interface GoalCommandArgs {
9
+ action: "set" | "status" | "pause" | "resume" | "clear" | "update" | "history";
10
+ objective?: string;
11
+ budget?: Partial<BudgetConfig>;
12
+ }
13
+
14
+ export function parseGoalArgs(raw: string): GoalCommandArgs {
15
+ const trimmed = raw.trim().toLowerCase();
16
+ const fullRaw = raw.trim();
17
+
18
+ // Subcommands without objective
19
+ if (trimmed === "" || trimmed === "status") {
20
+ return { action: "status" };
21
+ }
22
+ if (trimmed === "pause") {
23
+ return { action: "pause" };
24
+ }
25
+ if (trimmed === "resume") {
26
+ return { action: "resume" };
27
+ }
28
+ if (trimmed === "clear") {
29
+ return { action: "clear" };
30
+ }
31
+ if (trimmed === "history") {
32
+ return { action: "history" };
33
+ }
34
+
35
+ // /goal update <new objective>
36
+ if (trimmed.startsWith("update ")) {
37
+ return { action: "update", objective: fullRaw.slice(UPDATE_PREFIX_LENGTH).trim() };
38
+ }
39
+ // /goal update (without argument) → 报错
40
+ if (trimmed === "update") {
41
+ return { action: "update" };
42
+ }
43
+
44
+ // /goal <objective> [--tokens N] [--timeout N] [--max-turns N] [--max-stall N]
45
+ // 只匹配已知 flag,避免误删 objective 中的 -- 文本
46
+ const knownFlags = /--(?:tokens|timeout|max-turns|max-stall)\s+\d+/g;
47
+ const objective = fullRaw.replace(knownFlags, "").trim();
48
+ const budget: Partial<BudgetConfig> = {};
49
+
50
+ const tokenMatch = fullRaw.match(/--tokens\s+(\d+)/);
51
+ if (tokenMatch) {
52
+ const val = parseInt(tokenMatch[1]!, 10);
53
+ if (!isNaN(val) && val > 0) budget.tokenBudget = val;
54
+ }
55
+
56
+ const timeMatch = fullRaw.match(/--timeout\s+(\d+)/);
57
+ if (timeMatch) {
58
+ const val = parseInt(timeMatch[1]!, 10);
59
+ if (!isNaN(val) && val > 0) budget.timeBudgetMinutes = val;
60
+ }
61
+
62
+ const maxTurnsMatch = fullRaw.match(/--max-turns\s+(\d+)/);
63
+ if (maxTurnsMatch) {
64
+ const val = parseInt(maxTurnsMatch[1]!, 10);
65
+ if (!isNaN(val)) budget.maxTurns = Math.max(1, Math.min(val, MAX_TURNS_CAP));
66
+ }
67
+
68
+ const maxStallMatch = fullRaw.match(/--max-stall\s+(\d+)/);
69
+ if (maxStallMatch) {
70
+ const val = parseInt(maxStallMatch[1]!, 10);
71
+ if (!isNaN(val)) budget.maxStallTurns = Math.max(1, Math.min(val, MAX_STALL_CAP));
72
+ }
73
+
74
+ if (!objective) {
75
+ return { action: "status" };
76
+ }
77
+
78
+ return { action: "set", objective, budget };
79
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Goal 扩展语义常量
3
+ *
4
+ * 所有数字的含义都通过命名自解释,避免 magic number。
5
+ */
6
+
7
+ // ── 时间换算 ────────────────────────────────────────
8
+
9
+ export const SECONDS_PER_MINUTE = 60;
10
+ export const MS_PER_SECOND = 1000;
11
+
12
+ // ── 预算比例阈值 (0-1) ──────────────────────────────
13
+
14
+ export const BUDGET_RATIO_HIGH = 0.9; // 90% — 触发预警/收尾 steering
15
+ export const BUDGET_RATIO_LOW = 0.7; // 70% — 触发提醒
16
+ export const BUDGET_RATIO_TIGHT = 0.8; // 80% — 预算紧张,优先 steer
17
+ export const CONTEXT_USAGE_RATIO_LIMIT = 0.85; // 85% — 上下文空间不足阈值
18
+
19
+ // ── 预算百分比阈值 (0-100) ──────────────────────────
20
+
21
+ export const BUDGET_PERCENT_HIGH = 90; // widget 颜色变红
22
+ export const BUDGET_PERCENT_LOW = 70; // widget 颜色变黄
23
+
24
+ // ── 长度/数量上限 ───────────────────────────────────
25
+
26
+ export const MAX_TURNS_CAP = 100; // maxTurns 上限
27
+ export const MAX_STALL_CAP = 20; // maxStallTurns 上限
28
+ export const UPDATE_PREFIX_LENGTH = 7; // "update ".length
29
+
30
+ // ── 百分比换算因子 ──────────────────────────────────
31
+
32
+ export const PERCENT_FACTOR = 100;
33
+
34
+ // ── TUI 显示 ────────────────────────────────────────
35
+
36
+ export const PROGRESS_BAR_DEFAULT_WIDTH = 10;
37
+ export const OBJECTIVE_DISPLAY_LIMIT = 80;
38
+ export const OBJECTIVE_TRUNCATE_KEEP = 77; // DISPLAY_LIMIT - 3 for "..."
39
+
40
+ // ── 停滞/清理阈值 ────────────────────────────────────
41
+
42
+ export const TASK_STALL_TURN_THRESHOLD = 10; // task/subtask 停滞提醒阈值(turn 数)
43
+ export const AUTO_CLEAR_TURNS = 2; // 终态后自动清理轮数
44
+ export const MAX_HISTORY_ENTRIES = 20; // goal-history entry GC 上限