@zhushanwen/pi-goal 0.2.0 → 0.4.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 +13 -12
- package/package.json +8 -4
- package/src/__tests__/command-adapter.test.ts +404 -0
- package/src/__tests__/deserialize-state.test.ts +80 -0
- package/src/__tests__/event-adapter.test.ts +339 -0
- package/src/__tests__/goal-control-adapter.test.ts +303 -0
- package/src/__tests__/index.test.ts +119 -0
- package/src/__tests__/service.test.ts +317 -0
- package/src/__tests__/session.test.ts +220 -0
- package/src/__tests__/stale-checker.test.ts +78 -0
- package/src/__tests__/stubs/pi-sdk.ts +16 -0
- package/src/__tests__/stubs/typebox.ts +14 -0
- package/src/adapters/command-adapter.ts +360 -0
- package/src/adapters/event-adapter.ts +17 -0
- package/src/adapters/event-handlers/agent-end.ts +177 -0
- package/src/adapters/event-handlers/agent-start.ts +17 -0
- package/src/adapters/event-handlers/before-agent-start.ts +115 -0
- package/src/adapters/event-handlers/message-end.ts +37 -0
- package/src/adapters/event-handlers/session-start.ts +24 -0
- package/src/adapters/event-handlers/shared.ts +46 -0
- package/src/adapters/event-handlers/turn-end.ts +37 -0
- package/src/adapters/goal-control-adapter.ts +296 -0
- package/src/adapters/ports.ts +89 -0
- package/src/commands.ts +7 -19
- package/src/constants.ts +1 -3
- package/src/engine/__tests__/budget.test.ts +182 -0
- package/src/engine/__tests__/goal.test.ts +126 -0
- package/src/engine/budget.ts +146 -0
- package/src/engine/goal.ts +62 -0
- package/src/engine/types.ts +80 -0
- package/src/index.ts +110 -333
- package/src/persistence.ts +83 -0
- package/src/ports.ts +68 -0
- package/src/projection/__tests__/prompts.test.ts +245 -0
- package/src/projection/__tests__/widget.test.ts +255 -0
- package/src/projection/prompts.ts +250 -0
- package/src/projection/widget.ts +254 -0
- package/src/service.ts +276 -0
- package/src/session.ts +114 -0
- package/src/action-handlers.ts +0 -341
- package/src/agent-end-handler.ts +0 -304
- package/src/before-agent-start-handler.ts +0 -182
- package/src/budget.ts +0 -159
- package/src/command-handler.ts +0 -280
- package/src/state.ts +0 -250
- package/src/templates.ts +0 -232
- package/src/tool-handler.ts +0 -252
- package/src/widget.ts +0 -186
package/src/state.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
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
|
-
// ── Cross-extension API type ─────────────────────────────
|
|
19
|
-
|
|
20
|
-
/** Budget options for external goal initialization. */
|
|
21
|
-
export interface GoalExternalBudget {
|
|
22
|
-
tokenBudget?: number;
|
|
23
|
-
timeBudgetMinutes?: number;
|
|
24
|
-
maxTurns?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Function signature for programmatically initializing a goal from another extension.
|
|
29
|
-
* Exposed via `pi.__goalInit` by the goal extension.
|
|
30
|
-
*/
|
|
31
|
-
export type GoalExternalInit = (
|
|
32
|
-
objective: string,
|
|
33
|
-
tasks: string[],
|
|
34
|
-
budget?: GoalExternalBudget,
|
|
35
|
-
) => boolean;
|
|
36
|
-
|
|
37
|
-
// ── Goal 状态枚举 ──────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
export type GoalStatus =
|
|
40
|
-
| "active"
|
|
41
|
-
| "paused"
|
|
42
|
-
| "blocked"
|
|
43
|
-
| "complete"
|
|
44
|
-
| "budget_limited"
|
|
45
|
-
| "time_limited"
|
|
46
|
-
| "cancelled";
|
|
47
|
-
|
|
48
|
-
// 终态:不可被其他状态覆盖
|
|
49
|
-
const TERMINAL_STATUSES: ReadonlySet<GoalStatus> = new Set([
|
|
50
|
-
"complete",
|
|
51
|
-
"budget_limited",
|
|
52
|
-
"time_limited",
|
|
53
|
-
"cancelled",
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
// ── 任务数据结构 ──────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
|
|
59
|
-
|
|
60
|
-
export type SubtaskStatus = "pending" | "in_progress" | "completed";
|
|
61
|
-
|
|
62
|
-
export const SUBTASK_STATUSES: readonly SubtaskStatus[] = [
|
|
63
|
-
"pending",
|
|
64
|
-
"in_progress",
|
|
65
|
-
"completed",
|
|
66
|
-
] as const;
|
|
67
|
-
|
|
68
|
-
export interface Subtask {
|
|
69
|
-
id: number;
|
|
70
|
-
text: string;
|
|
71
|
-
status: SubtaskStatus;
|
|
72
|
-
lastUpdatedTurn: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const GOAL_TASK_STATUSES: readonly TaskStatus[] = [
|
|
76
|
-
"pending",
|
|
77
|
-
"in_progress",
|
|
78
|
-
"completed",
|
|
79
|
-
"cancelled",
|
|
80
|
-
] as const;
|
|
81
|
-
|
|
82
|
-
export function isTerminalTaskStatus(status: TaskStatus): boolean {
|
|
83
|
-
return status === "completed" || status === "cancelled";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface GoalTask {
|
|
87
|
-
id: number;
|
|
88
|
-
description: string;
|
|
89
|
-
status: TaskStatus;
|
|
90
|
-
evidence?: string; // 完成时的证据描述
|
|
91
|
-
subtasks?: Subtask[];
|
|
92
|
-
lastUpdatedTurn: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── 预算配置 ──────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
export interface BudgetConfig {
|
|
98
|
-
tokenBudget?: number; // token 预算上限 (undefined = 不限制)
|
|
99
|
-
timeBudgetMinutes?: number; // 时间预算 (分钟, undefined = 不限制)
|
|
100
|
-
maxStallTurns: number; // 连续无进展轮数阈值,触发 blocked
|
|
101
|
-
maxTurns: number; // 最大 turn 数上限
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── 运行时状态(也是持久化数据格式,保持统一)─────────────
|
|
105
|
-
|
|
106
|
-
export interface GoalRuntimeState {
|
|
107
|
-
goalId: string;
|
|
108
|
-
objective: string;
|
|
109
|
-
status: GoalStatus;
|
|
110
|
-
tasks: GoalTask[];
|
|
111
|
-
turnCount: number;
|
|
112
|
-
stallCount: number;
|
|
113
|
-
tokensUsed: number;
|
|
114
|
-
timeStartedAt: number; // Date.now() timestamp
|
|
115
|
-
timeUsedSeconds: number; // 累计使用秒数(不含当前活跃段)
|
|
116
|
-
budget: BudgetConfig;
|
|
117
|
-
lastProgressTurn: number; // 上次有进展的 turn number
|
|
118
|
-
budgetLimitSteeringSent: boolean; // 是否已发送预算耗尽 steering
|
|
119
|
-
objectiveUpdatedAt: number; // objective 最后更新时间
|
|
120
|
-
lastBlockerReason: string | null; // 上次 report_blocked 的原因,resume 时注入
|
|
121
|
-
budgetWarning70Sent: boolean; // 70% 预算预警已发送(token 或时间任一达 70%)
|
|
122
|
-
budgetWarning90Sent: boolean; // 90% 预算预警已发送
|
|
123
|
-
lastTurnTokensUsed: number; // 上一 turn 结束时的 tokensUsed,用于去抖检测
|
|
124
|
-
currentTurnIndex: number; // turn_end 粒度计数器
|
|
125
|
-
completedAtTurnIndex?: number; // 进入终态时的 currentTurnIndex
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── 默认值 ────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
export const DEFAULT_BUDGET: BudgetConfig = {
|
|
131
|
-
maxStallTurns: 5,
|
|
132
|
-
maxTurns: 50,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
export function createInitialState(objective: string, budget: Partial<BudgetConfig> = {}): GoalRuntimeState {
|
|
136
|
-
return {
|
|
137
|
-
goalId: crypto.randomUUID(),
|
|
138
|
-
objective,
|
|
139
|
-
status: "active",
|
|
140
|
-
tasks: [],
|
|
141
|
-
turnCount: 0,
|
|
142
|
-
stallCount: 0,
|
|
143
|
-
tokensUsed: 0,
|
|
144
|
-
timeStartedAt: Date.now(),
|
|
145
|
-
timeUsedSeconds: 0,
|
|
146
|
-
budget: { ...DEFAULT_BUDGET, ...budget },
|
|
147
|
-
lastProgressTurn: 0,
|
|
148
|
-
budgetLimitSteeringSent: false,
|
|
149
|
-
objectiveUpdatedAt: Date.now(),
|
|
150
|
-
lastBlockerReason: null,
|
|
151
|
-
budgetWarning70Sent: false,
|
|
152
|
-
budgetWarning90Sent: false,
|
|
153
|
-
lastTurnTokensUsed: 0,
|
|
154
|
-
currentTurnIndex: 0,
|
|
155
|
-
completedAtTurnIndex: undefined,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── 状态转换 ──────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* 安全的状态转换。终态不可被覆盖。
|
|
163
|
-
*/
|
|
164
|
-
export function transitionStatus(current: GoalStatus, next: GoalStatus): GoalStatus {
|
|
165
|
-
if (TERMINAL_STATUSES.has(current)) return current;
|
|
166
|
-
return next;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function isTerminalStatus(status: GoalStatus): boolean {
|
|
170
|
-
return TERMINAL_STATUSES.has(status);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function isActiveStatus(status: GoalStatus): boolean {
|
|
174
|
-
return status === "active";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ── 序列化(直接用相同类型,避免维护两个相同接口)──────
|
|
178
|
-
|
|
179
|
-
export function serializeState(state: GoalRuntimeState): GoalRuntimeState {
|
|
180
|
-
return {
|
|
181
|
-
...state,
|
|
182
|
-
tasks: state.tasks.map((t) => ({
|
|
183
|
-
...t,
|
|
184
|
-
subtasks: t.subtasks?.map((s) => ({ ...s })),
|
|
185
|
-
})),
|
|
186
|
-
budget: { ...state.budget },
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 反序列化,补全缺失字段的默认值(向后兼容旧格式数据)
|
|
192
|
-
*/
|
|
193
|
-
export function deserializeState(data: Record<string, unknown>): GoalRuntimeState {
|
|
194
|
-
return {
|
|
195
|
-
goalId: (data.goalId as string) ?? "",
|
|
196
|
-
objective: (data.objective as string) ?? "",
|
|
197
|
-
status: (data.status as GoalStatus) ?? "active",
|
|
198
|
-
tasks: ((data.tasks as Record<string, unknown>[]) ?? []).map((t: Record<string, unknown>) => {
|
|
199
|
-
if (!("status" in t)) {
|
|
200
|
-
throw new Error("Legacy goal-state format detected, session reset required");
|
|
201
|
-
}
|
|
202
|
-
const rawSubtasks = (t.subtasks ?? t.subTodos) as Record<string, unknown>[] | undefined;
|
|
203
|
-
const subtasks = Array.isArray(rawSubtasks)
|
|
204
|
-
? rawSubtasks
|
|
205
|
-
.filter((s) => typeof s.id === "number" && typeof s.text === "string" && typeof s.status === "string")
|
|
206
|
-
.map((s) => ({
|
|
207
|
-
id: s.id as number,
|
|
208
|
-
text: s.text as string,
|
|
209
|
-
status: s.status as SubtaskStatus,
|
|
210
|
-
lastUpdatedTurn: (s.lastUpdatedTurn as number) ?? 0,
|
|
211
|
-
}))
|
|
212
|
-
: undefined;
|
|
213
|
-
return { ...t, subtasks, lastUpdatedTurn: (t.lastUpdatedTurn as number) ?? 0 } as unknown as GoalTask;
|
|
214
|
-
}),
|
|
215
|
-
turnCount: (data.turnCount as number) ?? 0,
|
|
216
|
-
stallCount: (data.stallCount as number) ?? 0,
|
|
217
|
-
tokensUsed: (data.tokensUsed as number) ?? 0,
|
|
218
|
-
timeStartedAt: (data.timeStartedAt as number) ?? Date.now(),
|
|
219
|
-
timeUsedSeconds: (data.timeUsedSeconds as number) ?? 0,
|
|
220
|
-
budget: { ...DEFAULT_BUDGET, ...((data.budget as Partial<BudgetConfig>) ?? {}) },
|
|
221
|
-
lastProgressTurn: (data.lastProgressTurn as number) ?? 0,
|
|
222
|
-
budgetLimitSteeringSent: (data.budgetLimitSteeringSent as boolean) ?? false,
|
|
223
|
-
objectiveUpdatedAt: (data.objectiveUpdatedAt as number) ?? Date.now(),
|
|
224
|
-
lastBlockerReason: (data.lastBlockerReason as string | null) ?? null,
|
|
225
|
-
budgetWarning70Sent: (data.budgetWarning70Sent as boolean) ?? false,
|
|
226
|
-
budgetWarning90Sent: (data.budgetWarning90Sent as boolean) ?? false,
|
|
227
|
-
lastTurnTokensUsed: (data.lastTurnTokensUsed as number) ?? 0,
|
|
228
|
-
currentTurnIndex: (data.currentTurnIndex as number) ?? 0,
|
|
229
|
-
completedAtTurnIndex: data.completedAtTurnIndex as number | undefined,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ── 进度计算 ──────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
export function getCompletedCount(tasks: GoalTask[]): number {
|
|
236
|
-
return tasks.filter((t) => t.status === "completed").length;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function getIncompleteTasks(tasks: GoalTask[]): GoalTask[] {
|
|
240
|
-
return tasks.filter((t) => !isTerminalTaskStatus(t.status));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function getElapsedTimeSeconds(state: GoalRuntimeState): number {
|
|
244
|
-
if (isTerminalStatus(state.status) || state.status === "paused") return state.timeUsedSeconds;
|
|
245
|
-
return state.timeUsedSeconds + (Date.now() - state.timeStartedAt) / MS_PER_SECOND;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// getTokenUsagePercent 和 getTimeUsagePercent 已移至 budget.ts
|
|
249
|
-
// 保留 re-export 以便渐进迁移
|
|
250
|
-
export { getTimeUsagePercent,getTokenUsagePercent } from "./budget.js";
|
package/src/templates.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
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 { PERCENT_FACTOR, SECONDS_PER_MINUTE, TASK_STALL_TURN_THRESHOLD } from "./constants";
|
|
13
|
-
import type { GoalRuntimeState, GoalTask } from "./state";
|
|
14
|
-
import { getCompletedCount, getElapsedTimeSeconds,getIncompleteTasks } from "./state";
|
|
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} turns stalled` : "";
|
|
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 ? ` (remaining: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
|
|
41
|
-
: "Tasks: Not created. Call create_tasks immediately.";
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
`<goal_context>\n` +
|
|
45
|
-
`[GOAL] Turn ${state.currentTurnIndex}/${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 (replaces todo tool).\n` +
|
|
49
|
-
`Audit: Verify each requirement has authoritative evidence. Do not mark completed due to budget exhaustion, do not mark blocked due to difficulty.\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: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
|
|
66
|
-
: "All tasks completed.";
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
`<goal_context>\n` +
|
|
70
|
-
`[GOAL — ${limitType === "token" ? "TOKEN budget" : "time budget"} almost exhausted]\n\n` +
|
|
71
|
-
`<objective>\n${objective}\n</objective>\n\n` +
|
|
72
|
-
`Current progress: ${completedCount}/${total} tasks completed\n` +
|
|
73
|
-
`${incompleteSummary}\n` +
|
|
74
|
-
(limitType === "token"
|
|
75
|
-
? `Tokens used: ${state.tokensUsed} / ${state.budget.tokenBudget ?? "unknown"}\n`
|
|
76
|
-
: `Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s / ${state.budget.timeBudgetMinutes ?? "unknown"} min\n`) +
|
|
77
|
-
`\nYou must wrap up immediately:\n` +
|
|
78
|
-
`1. Use goal_manager's list_tasks to check remaining tasks\n` +
|
|
79
|
-
`2. Only mark tasks you have genuinely completed with evidence\n` +
|
|
80
|
-
`3. If the objective is met, call goal_manager's complete_goal\n` +
|
|
81
|
-
`4. Summarize current progress and remaining work\n` +
|
|
82
|
-
`Do not start new tasks. Do not mark completed due to budget exhaustion.\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 — Objective updated]\n\n` +
|
|
96
|
-
`Previous objective: ${escapedOld}\n` +
|
|
97
|
-
`<untrusted_objective>\n${newObjective}\n</untrusted_objective>\n\n` +
|
|
98
|
-
`This new objective supersedes all prior objective context. You must:\n` +
|
|
99
|
-
`1. Immediately stop working toward the old objective\n` +
|
|
100
|
-
`2. Re-evaluate the task list — call goal_manager's create_tasks to re-decompose if needed\n` +
|
|
101
|
-
`3. Only continue old work if it also serves the new objective\n` +
|
|
102
|
-
`4. Proceed with the new objective\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 mode activated]\n\n` +
|
|
118
|
-
`<objective>\n${objective}\n</objective>\n` +
|
|
119
|
-
`Status: ${state.status}\n` +
|
|
120
|
-
`Turn: ${state.currentTurnIndex}/${state.budget.maxTurns}${budgetInfo}\n` +
|
|
121
|
-
`Task progress: ${completedCount}/${total}\n\n` +
|
|
122
|
-
`Strict rules:\n` +
|
|
123
|
-
`1. First step: call goal_manager's create_tasks to decompose tasks (if not yet created)\n` +
|
|
124
|
-
`2. After completing a task, call update_tasks with status=completed and provide evidence\n` +
|
|
125
|
-
`3. Only call complete_goal with concrete evidence\n` +
|
|
126
|
-
`4. If blocked, call report_blocked\n` +
|
|
127
|
-
`5. In Goal mode, do not use the todo tool — use add_subtasks / update_subtasks for fine-grained tracking\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 reminder — tasks stalled]\n");
|
|
148
|
-
|
|
149
|
-
if (allTerminal) {
|
|
150
|
-
lines.push("All tasks completed but goal_manager is still open. Call complete_goal or cancel_goal.");
|
|
151
|
-
} else {
|
|
152
|
-
lines.push(`The following tasks have exceeded ${TASK_STALL_TURN_THRESHOLD} turns without update:\n`);
|
|
153
|
-
for (const item of staleTasks) {
|
|
154
|
-
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turns idle)`);
|
|
155
|
-
for (const s of item.staleSubtasks) {
|
|
156
|
-
lines.push(` - ${s.text} (${s.staleTurns} turns)`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
lines.push("\nCheck these tasks — call update_tasks to report progress or cancel tasks that are no longer needed.");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
lines.push(`\nObjective: ${objective}`);
|
|
163
|
-
lines.push(`Turn: ${state.currentTurnIndex}/${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(`Time: ${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 "No tasks yet.";
|
|
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(`In progress / Pending (${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 (${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 (${cancelled.length}):`);
|
|
227
|
-
for (const t of cancelled) lines.push(` ✗ #${t.id}: ${t.description}`);
|
|
228
|
-
}
|
|
229
|
-
const summary = `${completed.length}/${tasks.length} completed` + (cancelled.length > 0 ? `, ${cancelled.length} cancelled` : "");
|
|
230
|
-
lines.push(summary);
|
|
231
|
-
return lines.join("\n");
|
|
232
|
-
}
|