@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 +151 -0
- package/index.ts +1 -0
- package/package.json +24 -0
- package/src/budget.ts +159 -0
- package/src/commands.ts +79 -0
- package/src/constants.ts +44 -0
- package/src/index.ts +895 -0
- package/src/state.ts +231 -0
- package/src/templates.ts +232 -0
- package/src/tool-handler.ts +487 -0
- package/src/widget.ts +185 -0
package/src/state.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal 状态定义和管理
|
|
3
|
+
*
|
|
4
|
+
* 状态机参考 Codex /goal 的 6 种状态:
|
|
5
|
+
* Active → Paused (用户暂停)
|
|
6
|
+
* Active → Blocked (连续 stall)
|
|
7
|
+
* Active → Complete (目标达成)
|
|
8
|
+
* Active → BudgetLimited (token 预算耗尽)
|
|
9
|
+
* Active → TimeLimited (时间预算耗尽)
|
|
10
|
+
* Active → Cancelled (用户清除)
|
|
11
|
+
*
|
|
12
|
+
* 终态(不可被任何状态覆盖):Complete, BudgetLimited, TimeLimited, Cancelled
|
|
13
|
+
* Paused/Blocked 可被 Active 覆盖(用户 resume)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { MS_PER_SECOND } from "./constants";
|
|
17
|
+
|
|
18
|
+
// ── Goal 状态枚举 ──────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type GoalStatus =
|
|
21
|
+
| "active"
|
|
22
|
+
| "paused"
|
|
23
|
+
| "blocked"
|
|
24
|
+
| "complete"
|
|
25
|
+
| "budget_limited"
|
|
26
|
+
| "time_limited"
|
|
27
|
+
| "cancelled";
|
|
28
|
+
|
|
29
|
+
// 终态:不可被其他状态覆盖
|
|
30
|
+
const TERMINAL_STATUSES: ReadonlySet<GoalStatus> = new Set([
|
|
31
|
+
"complete",
|
|
32
|
+
"budget_limited",
|
|
33
|
+
"time_limited",
|
|
34
|
+
"cancelled",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// ── 任务数据结构 ──────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
|
|
40
|
+
|
|
41
|
+
export type SubtaskStatus = "pending" | "in_progress" | "completed";
|
|
42
|
+
|
|
43
|
+
export const SUBTASK_STATUSES: readonly SubtaskStatus[] = [
|
|
44
|
+
"pending",
|
|
45
|
+
"in_progress",
|
|
46
|
+
"completed",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export interface Subtask {
|
|
50
|
+
id: number;
|
|
51
|
+
text: string;
|
|
52
|
+
status: SubtaskStatus;
|
|
53
|
+
lastUpdatedTurn: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const GOAL_TASK_STATUSES: readonly TaskStatus[] = [
|
|
57
|
+
"pending",
|
|
58
|
+
"in_progress",
|
|
59
|
+
"completed",
|
|
60
|
+
"cancelled",
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
export function isTerminalTaskStatus(status: TaskStatus): boolean {
|
|
64
|
+
return status === "completed" || status === "cancelled";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GoalTask {
|
|
68
|
+
id: number;
|
|
69
|
+
description: string;
|
|
70
|
+
status: TaskStatus;
|
|
71
|
+
evidence?: string; // 完成时的证据描述
|
|
72
|
+
subtasks?: Subtask[];
|
|
73
|
+
lastUpdatedTurn: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 预算配置 ──────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface BudgetConfig {
|
|
79
|
+
tokenBudget?: number; // token 预算上限 (undefined = 不限制)
|
|
80
|
+
timeBudgetMinutes?: number; // 时间预算 (分钟, undefined = 不限制)
|
|
81
|
+
maxStallTurns: number; // 连续无进展轮数阈值,触发 blocked
|
|
82
|
+
maxTurns: number; // 最大 turn 数上限
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── 运行时状态(也是持久化数据格式,保持统一)─────────────
|
|
86
|
+
|
|
87
|
+
export interface GoalRuntimeState {
|
|
88
|
+
goalId: string;
|
|
89
|
+
objective: string;
|
|
90
|
+
status: GoalStatus;
|
|
91
|
+
tasks: GoalTask[];
|
|
92
|
+
turnCount: number;
|
|
93
|
+
stallCount: number;
|
|
94
|
+
tokensUsed: number;
|
|
95
|
+
timeStartedAt: number; // Date.now() timestamp
|
|
96
|
+
timeUsedSeconds: number; // 累计使用秒数(不含当前活跃段)
|
|
97
|
+
budget: BudgetConfig;
|
|
98
|
+
lastProgressTurn: number; // 上次有进展的 turn number
|
|
99
|
+
budgetLimitSteeringSent: boolean; // 是否已发送预算耗尽 steering
|
|
100
|
+
objectiveUpdatedAt: number; // objective 最后更新时间
|
|
101
|
+
lastBlockerReason: string | null; // 上次 report_blocked 的原因,resume 时注入
|
|
102
|
+
budgetWarning70Sent: boolean; // 70% 预算预警已发送(token 或时间任一达 70%)
|
|
103
|
+
budgetWarning90Sent: boolean; // 90% 预算预警已发送
|
|
104
|
+
lastTurnTokensUsed: number; // 上一 turn 结束时的 tokensUsed,用于去抖检测
|
|
105
|
+
currentTurnIndex: number; // turn_end 粒度计数器
|
|
106
|
+
completedAtTurnIndex?: number; // 进入终态时的 currentTurnIndex
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 默认值 ────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export const DEFAULT_BUDGET: BudgetConfig = {
|
|
112
|
+
maxStallTurns: 5,
|
|
113
|
+
maxTurns: 50,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export function createInitialState(objective: string, budget: Partial<BudgetConfig> = {}): GoalRuntimeState {
|
|
117
|
+
return {
|
|
118
|
+
goalId: crypto.randomUUID(),
|
|
119
|
+
objective,
|
|
120
|
+
status: "active",
|
|
121
|
+
tasks: [],
|
|
122
|
+
turnCount: 0,
|
|
123
|
+
stallCount: 0,
|
|
124
|
+
tokensUsed: 0,
|
|
125
|
+
timeStartedAt: Date.now(),
|
|
126
|
+
timeUsedSeconds: 0,
|
|
127
|
+
budget: { ...DEFAULT_BUDGET, ...budget },
|
|
128
|
+
lastProgressTurn: 0,
|
|
129
|
+
budgetLimitSteeringSent: false,
|
|
130
|
+
objectiveUpdatedAt: Date.now(),
|
|
131
|
+
lastBlockerReason: null,
|
|
132
|
+
budgetWarning70Sent: false,
|
|
133
|
+
budgetWarning90Sent: false,
|
|
134
|
+
lastTurnTokensUsed: 0,
|
|
135
|
+
currentTurnIndex: 0,
|
|
136
|
+
completedAtTurnIndex: undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── 状态转换 ──────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 安全的状态转换。终态不可被覆盖。
|
|
144
|
+
*/
|
|
145
|
+
export function transitionStatus(current: GoalStatus, next: GoalStatus): GoalStatus {
|
|
146
|
+
if (TERMINAL_STATUSES.has(current)) return current;
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isTerminalStatus(status: GoalStatus): boolean {
|
|
151
|
+
return TERMINAL_STATUSES.has(status);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isActiveStatus(status: GoalStatus): boolean {
|
|
155
|
+
return status === "active";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── 序列化(直接用相同类型,避免维护两个相同接口)──────
|
|
159
|
+
|
|
160
|
+
export function serializeState(state: GoalRuntimeState): GoalRuntimeState {
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
tasks: state.tasks.map((t) => ({
|
|
164
|
+
...t,
|
|
165
|
+
subtasks: t.subtasks?.map((s) => ({ ...s })),
|
|
166
|
+
})),
|
|
167
|
+
budget: { ...state.budget },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 反序列化,补全缺失字段的默认值(向后兼容旧格式数据)
|
|
173
|
+
*/
|
|
174
|
+
export function deserializeState(data: Record<string, unknown>): GoalRuntimeState {
|
|
175
|
+
return {
|
|
176
|
+
goalId: (data.goalId as string) ?? "",
|
|
177
|
+
objective: (data.objective as string) ?? "",
|
|
178
|
+
status: (data.status as GoalStatus) ?? "active",
|
|
179
|
+
tasks: ((data.tasks as Record<string, unknown>[]) ?? []).map((t: Record<string, unknown>) => {
|
|
180
|
+
if (!("status" in t)) {
|
|
181
|
+
throw new Error("Legacy goal-state format detected, session reset required");
|
|
182
|
+
}
|
|
183
|
+
const rawSubtasks = (t.subtasks ?? t.subTodos) as Record<string, unknown>[] | undefined;
|
|
184
|
+
const subtasks = Array.isArray(rawSubtasks)
|
|
185
|
+
? rawSubtasks
|
|
186
|
+
.filter((s) => typeof s.id === "number" && typeof s.text === "string" && typeof s.status === "string")
|
|
187
|
+
.map((s) => ({
|
|
188
|
+
id: s.id as number,
|
|
189
|
+
text: s.text as string,
|
|
190
|
+
status: s.status as SubtaskStatus,
|
|
191
|
+
lastUpdatedTurn: (s.lastUpdatedTurn as number) ?? 0,
|
|
192
|
+
}))
|
|
193
|
+
: undefined;
|
|
194
|
+
return { ...t, subtasks, lastUpdatedTurn: (t.lastUpdatedTurn as number) ?? 0 } as unknown as GoalTask;
|
|
195
|
+
}),
|
|
196
|
+
turnCount: (data.turnCount as number) ?? 0,
|
|
197
|
+
stallCount: (data.stallCount as number) ?? 0,
|
|
198
|
+
tokensUsed: (data.tokensUsed as number) ?? 0,
|
|
199
|
+
timeStartedAt: (data.timeStartedAt as number) ?? Date.now(),
|
|
200
|
+
timeUsedSeconds: (data.timeUsedSeconds as number) ?? 0,
|
|
201
|
+
budget: { ...DEFAULT_BUDGET, ...((data.budget as Partial<BudgetConfig>) ?? {}) },
|
|
202
|
+
lastProgressTurn: (data.lastProgressTurn as number) ?? 0,
|
|
203
|
+
budgetLimitSteeringSent: (data.budgetLimitSteeringSent as boolean) ?? false,
|
|
204
|
+
objectiveUpdatedAt: (data.objectiveUpdatedAt as number) ?? Date.now(),
|
|
205
|
+
lastBlockerReason: (data.lastBlockerReason as string | null) ?? null,
|
|
206
|
+
budgetWarning70Sent: (data.budgetWarning70Sent as boolean) ?? false,
|
|
207
|
+
budgetWarning90Sent: (data.budgetWarning90Sent as boolean) ?? false,
|
|
208
|
+
lastTurnTokensUsed: (data.lastTurnTokensUsed as number) ?? 0,
|
|
209
|
+
currentTurnIndex: (data.currentTurnIndex as number) ?? 0,
|
|
210
|
+
completedAtTurnIndex: data.completedAtTurnIndex as number | undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── 进度计算 ──────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
export function getCompletedCount(tasks: GoalTask[]): number {
|
|
217
|
+
return tasks.filter((t) => t.status === "completed").length;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getIncompleteTasks(tasks: GoalTask[]): GoalTask[] {
|
|
221
|
+
return tasks.filter((t) => !isTerminalTaskStatus(t.status));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getElapsedTimeSeconds(state: GoalRuntimeState): number {
|
|
225
|
+
if (isTerminalStatus(state.status) || state.status === "paused") return state.timeUsedSeconds;
|
|
226
|
+
return state.timeUsedSeconds + (Date.now() - state.timeStartedAt) / MS_PER_SECOND;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// getTokenUsagePercent 和 getTimeUsagePercent 已移至 budget.ts
|
|
230
|
+
// 保留 re-export 以便渐进迁移
|
|
231
|
+
export { getTokenUsagePercent, getTimeUsagePercent } from "./budget.js";
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steering prompt 模板
|
|
3
|
+
*
|
|
4
|
+
* 参考 Codex /goal 的三套模板:
|
|
5
|
+
* 1. continuation.md — 每个 continuation turn 开始时注入
|
|
6
|
+
* 2. budget_limit.md — 首次达到 token budget 时注入
|
|
7
|
+
* 3. objective_updated.md — 外部修改了 goal objective 时注入
|
|
8
|
+
*
|
|
9
|
+
* 所有模板中的 objective 文本都经过 XML 转义,防止 prompt 注入。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { GoalRuntimeState, GoalTask } from "./state";
|
|
13
|
+
import { getIncompleteTasks, getCompletedCount, getElapsedTimeSeconds } from "./state";
|
|
14
|
+
import { SECONDS_PER_MINUTE, PERCENT_FACTOR, TASK_STALL_TURN_THRESHOLD } from "./constants";
|
|
15
|
+
|
|
16
|
+
// ── XML 转义(防止 objective 中的 XML 标签破坏 prompt 结构)──
|
|
17
|
+
|
|
18
|
+
function escapeXmlText(input: string): string {
|
|
19
|
+
return input
|
|
20
|
+
.replace(/&/g, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Continuation Prompt ───────────────────────────────
|
|
26
|
+
// 精简版,对标 Codex ~500 chars。详细信息在 before_agent_start 注入。
|
|
27
|
+
|
|
28
|
+
export function continuationPrompt(state: GoalRuntimeState): string {
|
|
29
|
+
const objective = escapeXmlText(state.objective);
|
|
30
|
+
const incomplete = getIncompleteTasks(state.tasks);
|
|
31
|
+
const completedCount = getCompletedCount(state.tasks);
|
|
32
|
+
const total = state.tasks.length;
|
|
33
|
+
|
|
34
|
+
// Budget info (single line, Codex style)
|
|
35
|
+
const budgetLine = formatBudgetLine(state);
|
|
36
|
+
const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns}轮无进展` : "";
|
|
37
|
+
|
|
38
|
+
// Task summary (only IDs, not full descriptions — descriptions in before_agent_start)
|
|
39
|
+
const taskLine = total > 0
|
|
40
|
+
? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (剩余: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
|
|
41
|
+
: "Tasks: 未创建。请立即 create_tasks。";
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
`<goal_context>\n` +
|
|
45
|
+
`[GOAL] Turn ${state.turnCount}/${state.budget.maxTurns}${budgetLine}${stallLine}\n` +
|
|
46
|
+
`<objective>${objective}</objective>\n` +
|
|
47
|
+
`${taskLine}\n` +
|
|
48
|
+
`Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (替代 todo 工具).\n` +
|
|
49
|
+
`Audit: 逐项验证每个需求有权威证据。不因预算耗尽标记完成,不因困难标记阻塞。\n` +
|
|
50
|
+
`</goal_context>`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Budget Limit Prompt ───────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function budgetLimitPrompt(state: GoalRuntimeState, limitType: "token" | "time"): string {
|
|
57
|
+
const objective = escapeXmlText(state.objective);
|
|
58
|
+
const completedCount = getCompletedCount(state.tasks);
|
|
59
|
+
const total = state.tasks.length;
|
|
60
|
+
const elapsed = getElapsedTimeSeconds(state);
|
|
61
|
+
|
|
62
|
+
const incomplete = getIncompleteTasks(state.tasks);
|
|
63
|
+
const incompleteSummary =
|
|
64
|
+
incomplete.length > 0
|
|
65
|
+
? `未完成: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
|
|
66
|
+
: "所有任务已完成。";
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
`<goal_context>\n` +
|
|
70
|
+
`[GOAL — ${limitType === "token" ? "TOKEN 预算" : "时间预算"}即将耗尽]\n\n` +
|
|
71
|
+
`<objective>\n${objective}\n</objective>\n\n` +
|
|
72
|
+
`当前进度: ${completedCount}/${total} 任务完成\n` +
|
|
73
|
+
`${incompleteSummary}\n` +
|
|
74
|
+
(limitType === "token"
|
|
75
|
+
? `Token 已使用: ${state.tokensUsed} / ${state.budget.tokenBudget ?? "未知"}\n`
|
|
76
|
+
: `已用时间: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}分${Math.floor(elapsed % SECONDS_PER_MINUTE)}秒 / ${state.budget.timeBudgetMinutes ?? "未知"}分钟\n`) +
|
|
77
|
+
`\n你必须立即收尾:\n` +
|
|
78
|
+
`1. 用 goal_manager 的 list_tasks 查看剩余任务\n` +
|
|
79
|
+
`2. 只标记你真正完成且有证据的任务\n` +
|
|
80
|
+
`3. 如果目标已达成,调用 goal_manager 的 complete_goal 完成目标\n` +
|
|
81
|
+
`4. 总结当前进度和剩余工作\n` +
|
|
82
|
+
`不要再开始新任务。不要因为预算耗尽就标记完成。\n` +
|
|
83
|
+
`</goal_context>`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Objective Updated Prompt ──────────────────────────
|
|
88
|
+
|
|
89
|
+
export function objectiveUpdatedPrompt(state: GoalRuntimeState, oldObjective: string): string {
|
|
90
|
+
const newObjective = escapeXmlText(state.objective);
|
|
91
|
+
const escapedOld = escapeXmlText(oldObjective);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
`<goal_context>\n` +
|
|
95
|
+
`[GOAL — 目标已更新]\n\n` +
|
|
96
|
+
`旧目标: ${escapedOld}\n` +
|
|
97
|
+
`<untrusted_objective>\n${newObjective}\n</untrusted_objective>\n\n` +
|
|
98
|
+
`这个新目标取代了之前的所有目标上下文。你需要:\n` +
|
|
99
|
+
`1. 立即停止朝旧目标方向的工作\n` +
|
|
100
|
+
`2. 重新评估任务清单,必要时调用 goal_manager 的 create_tasks 重新拆分\n` +
|
|
101
|
+
`3. 只在旧目标的工作也对新目标有帮助时才继续\n` +
|
|
102
|
+
`4. 按照新目标继续工作\n` +
|
|
103
|
+
`</goal_context>`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Context Injection Prompt (before_agent_start) ─────
|
|
108
|
+
|
|
109
|
+
export function contextInjectionPrompt(state: GoalRuntimeState): string {
|
|
110
|
+
const objective = escapeXmlText(state.objective);
|
|
111
|
+
const completedCount = getCompletedCount(state.tasks);
|
|
112
|
+
const total = state.tasks.length;
|
|
113
|
+
const budgetInfo = formatBudgetInfo(state);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
`<goal_context>\n` +
|
|
117
|
+
`[GOAL 模式已激活]\n\n` +
|
|
118
|
+
`<objective>\n${objective}\n</objective>\n` +
|
|
119
|
+
`状态: ${state.status}\n` +
|
|
120
|
+
`轮次: ${state.turnCount}/${state.budget.maxTurns}${budgetInfo}\n` +
|
|
121
|
+
`任务进度: ${completedCount}/${total}\n\n` +
|
|
122
|
+
`严格规则:\n` +
|
|
123
|
+
`1. 第一步必须调用 goal_manager 的 create_tasks 拆分任务(如果尚未创建)\n` +
|
|
124
|
+
`2. 每完成一个任务调用 update_tasks 将状态设为 completed,并提供 evidence\n` +
|
|
125
|
+
`3. 只有提供具体证据时才能调用 complete_goal\n` +
|
|
126
|
+
`4. 遇到阻塞调用 report_blocked\n` +
|
|
127
|
+
`5. Goal 模式下不要使用 todo 工具,使用 add_subtasks / update_subtasks 追踪细粒度步骤\n` +
|
|
128
|
+
`</goal_context>`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Staleness Reminder Prompt ────────────────────────
|
|
133
|
+
|
|
134
|
+
export function stalenessReminderPrompt(
|
|
135
|
+
state: GoalRuntimeState,
|
|
136
|
+
staleTasks: Array<{
|
|
137
|
+
task: GoalTask;
|
|
138
|
+
staleTurns: number;
|
|
139
|
+
staleSubtasks: Array<{ text: string; staleTurns: number }>;
|
|
140
|
+
}>,
|
|
141
|
+
allTerminal: boolean,
|
|
142
|
+
): string {
|
|
143
|
+
const objective = escapeXmlText(state.objective);
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
|
|
146
|
+
lines.push("<goal_context>");
|
|
147
|
+
lines.push("[GOAL 提醒 — 有任务停滞]\n");
|
|
148
|
+
|
|
149
|
+
if (allTerminal) {
|
|
150
|
+
lines.push("所有任务已完成,但 goal_manager 未关闭。请调用 complete_goal 或 cancel_goal。");
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(`以下任务已超过 ${TASK_STALL_TURN_THRESHOLD} turn 未更新:\n`);
|
|
153
|
+
for (const item of staleTasks) {
|
|
154
|
+
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turn 未操作)`);
|
|
155
|
+
for (const s of item.staleSubtasks) {
|
|
156
|
+
lines.push(` - ${s.text} (${s.staleTurns} turn)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
lines.push("\n请检查这些任务的状态,调用 update_tasks 更新进展或 cancel 不再需要的任务。");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines.push(`\n目标: ${objective}`);
|
|
163
|
+
lines.push(`Turn: ${state.turnCount}/${state.budget.maxTurns}`);
|
|
164
|
+
lines.push("</goal_context>");
|
|
165
|
+
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Helpers ───────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function formatBudgetInfo(state: GoalRuntimeState): string {
|
|
172
|
+
const parts: string[] = [];
|
|
173
|
+
if (state.budget.tokenBudget) {
|
|
174
|
+
const pct = Math.round((state.tokensUsed / state.budget.tokenBudget) * PERCENT_FACTOR);
|
|
175
|
+
parts.push(`Token: ${pct}%`);
|
|
176
|
+
}
|
|
177
|
+
if (state.budget.timeBudgetMinutes) {
|
|
178
|
+
const elapsed = getElapsedTimeSeconds(state);
|
|
179
|
+
const pct = Math.round((elapsed / (state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE)) * PERCENT_FACTOR);
|
|
180
|
+
parts.push(`时间: ${pct}%`);
|
|
181
|
+
}
|
|
182
|
+
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatBudgetLine(state: GoalRuntimeState): string {
|
|
186
|
+
const parts: string[] = [];
|
|
187
|
+
if (state.budget.tokenBudget) {
|
|
188
|
+
const remaining = Math.max(state.budget.tokenBudget - state.tokensUsed, 0);
|
|
189
|
+
parts.push(`Tokens: ${remaining}/${state.budget.tokenBudget}`);
|
|
190
|
+
}
|
|
191
|
+
if (state.budget.timeBudgetMinutes) {
|
|
192
|
+
const elapsed = getElapsedTimeSeconds(state);
|
|
193
|
+
const remaining = Math.max(state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE - elapsed, 0);
|
|
194
|
+
parts.push(`Time: ${Math.floor(remaining / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m`);
|
|
195
|
+
}
|
|
196
|
+
return parts.length > 0 ? ` | ${parts.join(" ")}` : "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatTaskList(tasks: GoalTask[]): string {
|
|
200
|
+
if (tasks.length === 0) return "暂无任务。";
|
|
201
|
+
const completed = tasks.filter(t => t.status === "completed");
|
|
202
|
+
const active = tasks.filter(t => t.status === "in_progress" || t.status === "pending");
|
|
203
|
+
const cancelled = tasks.filter(t => t.status === "cancelled");
|
|
204
|
+
const lines: string[] = [];
|
|
205
|
+
if (active.length > 0) {
|
|
206
|
+
lines.push(`进行中/待执行 (${active.length}):`);
|
|
207
|
+
for (const t of active) {
|
|
208
|
+
const icon = t.status === "in_progress" ? "●" : "☐";
|
|
209
|
+
lines.push(` ${icon} #${t.id}: ${t.description}`);
|
|
210
|
+
if (t.subtasks && t.subtasks.length > 0) {
|
|
211
|
+
for (const s of t.subtasks) {
|
|
212
|
+
const sIcon = s.status === "completed" ? "✓" : s.status === "in_progress" ? "●" : "○";
|
|
213
|
+
lines.push(` ${sIcon} #${t.id}.${s.id}: ${s.text}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (completed.length > 0) {
|
|
219
|
+
lines.push(`已完成 (${completed.length}):`);
|
|
220
|
+
for (const t of completed) {
|
|
221
|
+
const evidence = t.evidence ? ` — ${t.evidence}` : "";
|
|
222
|
+
lines.push(` ✓ #${t.id}: ${t.description}${evidence}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (cancelled.length > 0) {
|
|
226
|
+
lines.push(`已取消 (${cancelled.length}):`);
|
|
227
|
+
for (const t of cancelled) lines.push(` ✗ #${t.id}: ${t.description}`);
|
|
228
|
+
}
|
|
229
|
+
const summary = `${completed.length}/${tasks.length} 完成` + (cancelled.length > 0 ? `, ${cancelled.length} 已取消` : "");
|
|
230
|
+
lines.push(summary);
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|