@zhushanwen/pi-goal 0.1.2 → 0.1.4
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/package.json +1 -1
- package/src/budget.ts +1 -1
- package/src/index.ts +89 -94
- package/src/templates.ts +49 -49
- package/src/tool-handler.ts +40 -42
- package/src/widget.ts +16 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/budget.ts
CHANGED
|
@@ -147,7 +147,7 @@ export function checkProgress(state: GoalRuntimeState, tasksCompletedAtStart: nu
|
|
|
147
147
|
return {
|
|
148
148
|
allTasksDone: totalCount > 0 && incomplete.length === 0 && completedCount > 0,
|
|
149
149
|
noTasksCreated: totalCount === 0,
|
|
150
|
-
maxTurnsReached: state.
|
|
150
|
+
maxTurnsReached: state.currentTurnIndex >= state.budget.maxTurns,
|
|
151
151
|
isStalled: progressThisRound === 0,
|
|
152
152
|
budgetTight: Boolean(
|
|
153
153
|
state.budget.tokenBudget &&
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 健壮性保障:
|
|
13
13
|
* - goalId snapshot 防止旧回调操作新 goal
|
|
14
14
|
* - 时间累计统一由 persistState 管理,无双写
|
|
15
|
-
* -
|
|
15
|
+
* - before_agent_start 注入 context,agent_end 负责 continuation(预算检查/进度评估/续跑)
|
|
16
16
|
* - deserializeState 向后兼容旧格式
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -150,19 +150,19 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
150
150
|
switch (parsed.action) {
|
|
151
151
|
case "status": {
|
|
152
152
|
if (!session.state) {
|
|
153
|
-
ctx.ui.notify("Goal
|
|
153
|
+
ctx.ui.notify("Goal mode not active. Use /goal <objective> to start.", "info");
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
const completed = getCompletedCount(session.state.tasks);
|
|
157
157
|
const total = session.state.tasks.length;
|
|
158
158
|
const elapsed = getElapsedTimeSeconds(session.state);
|
|
159
159
|
const lines = [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
`Objective: ${session.state.objective}`,
|
|
161
|
+
`Status: ${session.state.status}`,
|
|
162
|
+
`Turn: ${session.state.currentTurnIndex}/${session.state.budget.maxTurns}`,
|
|
163
|
+
`Tasks: ${completed}/${total} completed`,
|
|
164
|
+
`Stall turns: ${session.state.stallCount}`,
|
|
165
|
+
`Time elapsed: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`,
|
|
166
166
|
session.state.budget.tokenBudget ? `Token: ${session.state.tokensUsed}/${session.state.budget.tokenBudget}` : null,
|
|
167
167
|
`Goal ID: ${session.state.goalId}`,
|
|
168
168
|
].filter(Boolean);
|
|
@@ -172,31 +172,31 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
172
172
|
|
|
173
173
|
case "pause": {
|
|
174
174
|
if (!session.state) {
|
|
175
|
-
ctx.ui.notify("Goal
|
|
175
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
178
|
if (isTerminalStatus(session.state.status)) {
|
|
179
|
-
ctx.ui.notify(`Goal
|
|
179
|
+
ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot pause.`, "warning");
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
session.state.status = transitionStatus(session.state.status, "paused");
|
|
183
183
|
persistGoalState(pi, session, ctx);
|
|
184
184
|
updateWidget(session, ctx);
|
|
185
|
-
ctx.ui.notify("Goal
|
|
185
|
+
ctx.ui.notify("Goal paused. Use /goal resume to continue.", "info");
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
case "resume": {
|
|
190
190
|
if (!session.state) {
|
|
191
|
-
ctx.ui.notify("Goal
|
|
191
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
if (isTerminalStatus(session.state.status)) {
|
|
195
|
-
ctx.ui.notify(`Goal
|
|
195
|
+
ctx.ui.notify(`Goal is in terminal state (${session.state.status}), cannot resume.`, "warning");
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
if (session.state.status !== "paused" && session.state.status !== "blocked") {
|
|
199
|
-
ctx.ui.notify("Goal
|
|
199
|
+
ctx.ui.notify("Goal is not paused or blocked, no need to resume.", "info");
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
202
|
session.state.status = "active";
|
|
@@ -210,7 +210,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
210
210
|
session.state.status = transitionStatus(session.state.status, dim === "token" ? "budget_limited" : "time_limited");
|
|
211
211
|
persistGoalState(pi, session, ctx);
|
|
212
212
|
updateWidget(session, ctx);
|
|
213
|
-
ctx.ui.notify(`${dim === "token" ? "Token" : "
|
|
213
|
+
ctx.ui.notify(`${dim === "token" ? "Token" : "Time"} budget exhausted, cannot resume. Use /goal clear to reset.`, "warning");
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -220,17 +220,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
220
220
|
const incomplete = getIncompleteTasks(session.state.tasks);
|
|
221
221
|
if (incomplete.length > 0) {
|
|
222
222
|
pi.sendUserMessage(
|
|
223
|
-
`Goal
|
|
223
|
+
`Goal resumed. Continuing with ${incomplete.length} remaining tasks.` +
|
|
224
224
|
(session.state.lastBlockerReason ? `
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
Previous blocker: ${session.state.lastBlockerReason}. Try a different approach.` : "") +
|
|
227
227
|
`
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
Objective: ${session.state.objective}`,
|
|
230
230
|
{ deliverAs: "followUp" },
|
|
231
231
|
);
|
|
232
232
|
} else {
|
|
233
|
-
ctx.ui.notify("
|
|
233
|
+
ctx.ui.notify("All tasks completed.", "info");
|
|
234
234
|
}
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
@@ -250,13 +250,13 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
250
250
|
}>>;
|
|
251
251
|
|
|
252
252
|
if (historyEntries.length === 0) {
|
|
253
|
-
ctx.ui.notify("
|
|
253
|
+
ctx.ui.notify("No goal history", "info");
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// 按时间倒序
|
|
258
258
|
const sorted = [...historyEntries].reverse();
|
|
259
|
-
const lines: string[] = ["
|
|
259
|
+
const lines: string[] = ["Goal history:\n"];
|
|
260
260
|
for (let i = 0; i < sorted.length; i++) {
|
|
261
261
|
const h = sorted[i]!.data;
|
|
262
262
|
if (!h) continue;
|
|
@@ -271,7 +271,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
271
271
|
const mins = Math.floor(h.elapsedSeconds / SECONDS_PER_MINUTE);
|
|
272
272
|
const secs = Math.floor(h.elapsedSeconds % SECONDS_PER_MINUTE);
|
|
273
273
|
lines.push(`${i + 1}. ${statusIcon} ${objDisplay}`);
|
|
274
|
-
lines.push(` ${h.completedTasks}/${h.totalTasks}
|
|
274
|
+
lines.push(` ${h.completedTasks}/${h.totalTasks} tasks | ${mins}m${secs}s | ${h.status}`);
|
|
275
275
|
}
|
|
276
276
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
277
277
|
return;
|
|
@@ -279,7 +279,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
279
279
|
|
|
280
280
|
case "clear": {
|
|
281
281
|
if (!session.state) {
|
|
282
|
-
ctx.ui.notify("Goal
|
|
282
|
+
ctx.ui.notify("Goal mode not active.", "info");
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
session.state.status = "cancelled";
|
|
@@ -287,17 +287,17 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
287
287
|
writeGoalHistoryEntry(pi, session);
|
|
288
288
|
persistGoalState(pi, session, ctx);
|
|
289
289
|
clearGoalSession(session, ctx);
|
|
290
|
-
ctx.ui.notify("Goal
|
|
290
|
+
ctx.ui.notify("Goal cleared.", "info");
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
case "update": {
|
|
295
295
|
if (!session.state) {
|
|
296
|
-
ctx.ui.notify("Goal
|
|
296
|
+
ctx.ui.notify("Goal mode not active.", "warning");
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
if (!parsed.objective) {
|
|
300
|
-
ctx.ui.notify("
|
|
300
|
+
ctx.ui.notify("Usage: /goal update <new-objective>", "warning");
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
303
|
const oldObjective = session.state.objective;
|
|
@@ -306,6 +306,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
306
306
|
session.state.tasks = [];
|
|
307
307
|
session.state.stallCount = 0;
|
|
308
308
|
session.state.turnCount = 0;
|
|
309
|
+
session.state.currentTurnIndex = 0;
|
|
309
310
|
session.state.lastProgressTurn = 0;
|
|
310
311
|
session.state.budgetLimitSteeringSent = false;
|
|
311
312
|
session.state.budgetWarning70Sent = false;
|
|
@@ -313,7 +314,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
313
314
|
session.tasksCompletedAtAgentStart = 0;
|
|
314
315
|
persistGoalState(pi, session, ctx);
|
|
315
316
|
updateWidget(session, ctx);
|
|
316
|
-
ctx.ui.notify(
|
|
317
|
+
ctx.ui.notify(`Objective updated:\nPrevious: ${oldObjective}\nNew: ${parsed.objective}`, "info");
|
|
317
318
|
|
|
318
319
|
if (isActiveStatus(session.state.status)) {
|
|
319
320
|
pi.sendUserMessage(objectiveUpdatedPrompt(session.state, oldObjective), { deliverAs: "steer" });
|
|
@@ -323,16 +324,16 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
323
324
|
|
|
324
325
|
case "set": {
|
|
325
326
|
if (!parsed.objective) {
|
|
326
|
-
ctx.ui.notify("
|
|
327
|
+
ctx.ui.notify("Usage: /goal <objective> [--tokens N] [--timeout N]", "warning");
|
|
327
328
|
return;
|
|
328
329
|
}
|
|
329
330
|
if (!parsed.objective.trim()) {
|
|
330
|
-
ctx.ui.notify("
|
|
331
|
+
ctx.ui.notify("Objective cannot be empty.", "warning");
|
|
331
332
|
return;
|
|
332
333
|
}
|
|
333
334
|
if (session.state && !isTerminalStatus(session.state.status)) {
|
|
334
335
|
ctx.ui.notify(
|
|
335
|
-
|
|
336
|
+
`Cancelled previous Goal: ${session.state.objective}\n(new goal started)`,
|
|
336
337
|
"info",
|
|
337
338
|
);
|
|
338
339
|
session.state.status = "cancelled";
|
|
@@ -342,7 +343,7 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
if (parsed.budget?.tokenBudget !== undefined && parsed.budget.tokenBudget <= 0) {
|
|
345
|
-
ctx.ui.notify("Token
|
|
346
|
+
ctx.ui.notify("Token budget must be greater than 0.", "warning");
|
|
346
347
|
return;
|
|
347
348
|
}
|
|
348
349
|
const budget: Partial<BudgetConfig> = {};
|
|
@@ -359,11 +360,11 @@ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: s
|
|
|
359
360
|
updateWidget(session, ctx);
|
|
360
361
|
|
|
361
362
|
const budgetNotice: string[] = [];
|
|
362
|
-
if (budget.tokenBudget) budgetNotice.push(`Token
|
|
363
|
-
if (budget.timeBudgetMinutes) budgetNotice.push(
|
|
363
|
+
if (budget.tokenBudget) budgetNotice.push(`Token budget: ${budget.tokenBudget}`);
|
|
364
|
+
if (budget.timeBudgetMinutes) budgetNotice.push(`Time budget: ${budget.timeBudgetMinutes} min`);
|
|
364
365
|
const notice = [
|
|
365
|
-
`Goal
|
|
366
|
-
|
|
366
|
+
`Goal started: ${parsed.objective}`,
|
|
367
|
+
`Max turns: ${budget.maxTurns}`,
|
|
367
368
|
...budgetNotice,
|
|
368
369
|
].join("\n");
|
|
369
370
|
ctx.ui.notify(notice, "info");
|
|
@@ -477,11 +478,11 @@ async function handleBeforeAgentStart(pi: ExtensionAPI, session: GoalSession, ct
|
|
|
477
478
|
message: {
|
|
478
479
|
customType: "goal-context-exceeded",
|
|
479
480
|
content:
|
|
480
|
-
"[GOAL —
|
|
481
|
-
"1.
|
|
482
|
-
"2.
|
|
483
|
-
"3.
|
|
484
|
-
"
|
|
481
|
+
"[GOAL — context space low, must wrap up now]\n" +
|
|
482
|
+
"1. Use goal_manager's list_tasks to check remaining tasks\n" +
|
|
483
|
+
"2. Only mark tasks you genuinely completed with evidence\n" +
|
|
484
|
+
"3. Summarize current progress and remaining work\n" +
|
|
485
|
+
"Do not start new tasks.",
|
|
485
486
|
display: false,
|
|
486
487
|
},
|
|
487
488
|
};
|
|
@@ -510,7 +511,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
510
511
|
if (checkStale()) return;
|
|
511
512
|
updateWidget(session, ctx);
|
|
512
513
|
ctx.ui.notify(
|
|
513
|
-
|
|
514
|
+
`Objective completed ✓ (${getCompletedCount(session.state.tasks)}/${session.state.tasks.length} tasks, ${session.state.currentTurnIndex} turns)`,
|
|
514
515
|
"info",
|
|
515
516
|
);
|
|
516
517
|
return;
|
|
@@ -520,18 +521,12 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
520
521
|
persistGoalState(pi, session, ctx);
|
|
521
522
|
if (checkStale()) return;
|
|
522
523
|
updateWidget(session, ctx);
|
|
523
|
-
ctx.ui.notify("Goal
|
|
524
|
+
ctx.ui.notify("Goal blocked. Use /goal resume to continue or /goal clear to reset.", "warning");
|
|
524
525
|
return;
|
|
525
526
|
}
|
|
526
527
|
|
|
527
528
|
if (!isActiveStatus(session.state.status)) return;
|
|
528
529
|
|
|
529
|
-
// 防重入
|
|
530
|
-
if (session.hasPendingInjection) {
|
|
531
|
-
session.hasPendingInjection = false;
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
530
|
if (checkStale()) return;
|
|
536
531
|
|
|
537
532
|
// ── 预算策略(集中检查)──
|
|
@@ -542,10 +537,10 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
542
537
|
for (const w of budgetResult.warnings) {
|
|
543
538
|
if (w.type === "warning90") {
|
|
544
539
|
session.state.budgetWarning90Sent = true;
|
|
545
|
-
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "
|
|
540
|
+
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 90% used — start wrapping up.`, "warning");
|
|
546
541
|
} else if (w.type === "warning70") {
|
|
547
542
|
session.state.budgetWarning70Sent = true;
|
|
548
|
-
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "
|
|
543
|
+
ctx.ui.notify(`${w.dimension === "token" ? "Token" : "Time"} budget 70% used — keep scope in check.`, "info");
|
|
549
544
|
}
|
|
550
545
|
}
|
|
551
546
|
|
|
@@ -560,8 +555,8 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
560
555
|
updateWidget(session, ctx);
|
|
561
556
|
ctx.ui.notify(
|
|
562
557
|
dim === "token"
|
|
563
|
-
? "Token
|
|
564
|
-
:
|
|
558
|
+
? "Token budget exhausted, Goal terminated."
|
|
559
|
+
: `Time budget exhausted (${session.state.budget.timeBudgetMinutes} min), Goal terminated.`,
|
|
565
560
|
"warning",
|
|
566
561
|
);
|
|
567
562
|
return;
|
|
@@ -595,7 +590,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
595
590
|
if (checkStale()) return;
|
|
596
591
|
updateWidget(session, ctx);
|
|
597
592
|
ctx.ui.notify(
|
|
598
|
-
|
|
593
|
+
`All tasks completed, Goal auto-closed. (${progress.completedCount}/${progress.totalCount} tasks, ${session.state.currentTurnIndex} turns)`,
|
|
599
594
|
"info",
|
|
600
595
|
);
|
|
601
596
|
return;
|
|
@@ -603,15 +598,15 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
603
598
|
|
|
604
599
|
if (progress.budgetTight) {
|
|
605
600
|
pi.sendUserMessage(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
`\n\
|
|
601
|
+
`All tasks completed, token budget ${Math.round(session.state.tokensUsed / session.state.budget.tokenBudget! * PERCENT_FACTOR)}% used.` +
|
|
602
|
+
`Call goal_manager's complete_goal now with overall evidence.` +
|
|
603
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
609
604
|
{ deliverAs: "steer" },
|
|
610
605
|
);
|
|
611
606
|
} else {
|
|
612
607
|
pi.sendUserMessage(
|
|
613
|
-
|
|
614
|
-
`\n\
|
|
608
|
+
`All ${progress.totalCount} tasks completed. Call goal_manager's complete_goal with overall evidence.` +
|
|
609
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
615
610
|
{ deliverAs: "followUp" },
|
|
616
611
|
);
|
|
617
612
|
}
|
|
@@ -630,14 +625,14 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
630
625
|
if (checkStale()) return;
|
|
631
626
|
updateWidget(session, ctx);
|
|
632
627
|
ctx.ui.notify(
|
|
633
|
-
|
|
628
|
+
`Max turns reached (${session.state.budget.maxTurns}), LLM did not create task list.`,
|
|
634
629
|
"warning",
|
|
635
630
|
);
|
|
636
631
|
return;
|
|
637
632
|
}
|
|
638
633
|
pi.sendUserMessage(
|
|
639
|
-
|
|
640
|
-
`\n\
|
|
634
|
+
`No task list created yet. Call goal_manager's create_tasks immediately to decompose the work into verifiable task steps.` +
|
|
635
|
+
`\n\nObjective: ${session.state.objective}`,
|
|
641
636
|
{ deliverAs: "followUp" },
|
|
642
637
|
);
|
|
643
638
|
persistGoalState(pi, session, ctx);
|
|
@@ -655,7 +650,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
655
650
|
if (checkStale()) return;
|
|
656
651
|
updateWidget(session, ctx);
|
|
657
652
|
ctx.ui.notify(
|
|
658
|
-
|
|
653
|
+
`Max turns reached (${session.state.budget.maxTurns}), ${incomplete.length} tasks still incomplete.`,
|
|
659
654
|
"warning",
|
|
660
655
|
);
|
|
661
656
|
return;
|
|
@@ -666,7 +661,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
666
661
|
session.state.stallCount++;
|
|
667
662
|
} else {
|
|
668
663
|
session.state.stallCount = 0;
|
|
669
|
-
session.state.lastProgressTurn = session.state.
|
|
664
|
+
session.state.lastProgressTurn = session.state.currentTurnIndex;
|
|
670
665
|
}
|
|
671
666
|
|
|
672
667
|
if (session.state.stallCount >= session.state.budget.maxStallTurns) {
|
|
@@ -675,7 +670,7 @@ async function handleAgentEnd(pi: ExtensionAPI, session: GoalSession, ctx: Exten
|
|
|
675
670
|
if (checkStale()) return;
|
|
676
671
|
updateWidget(session, ctx);
|
|
677
672
|
ctx.ui.notify(
|
|
678
|
-
|
|
673
|
+
`${session.state.stallCount} consecutive turns without progress, Goal auto-blocked. Use /goal resume to continue or /goal clear to reset.`,
|
|
679
674
|
"warning",
|
|
680
675
|
);
|
|
681
676
|
return;
|
|
@@ -715,33 +710,33 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
715
710
|
name: "goal_manager",
|
|
716
711
|
label: "Goal Manager",
|
|
717
712
|
description:
|
|
718
|
-
"Goal
|
|
719
|
-
"\n\
|
|
720
|
-
"\n- create_tasks:
|
|
721
|
-
"\n- add_tasks:
|
|
722
|
-
"\n- update_tasks:
|
|
723
|
-
"\n- list_tasks:
|
|
724
|
-
"\n- complete_goal:
|
|
725
|
-
"\n- cancel_goal:
|
|
726
|
-
"\n- report_blocked:
|
|
727
|
-
"\n- add_subtasks:
|
|
728
|
-
"\n- update_subtasks:
|
|
729
|
-
"\n- delete_subtasks:
|
|
730
|
-
promptSnippet: "
|
|
713
|
+
"Goal mode task manager. This tool is only available after starting a goal via the /goal command. AI cannot trigger it proactively. If Goal mode is not active, calling this tool will error." +
|
|
714
|
+
"\n\nAvailable actions:" +
|
|
715
|
+
"\n- create_tasks: Decompose the objective into a task list (call once at goal start). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
|
|
716
|
+
"\n- add_tasks: Append new tasks to the existing list (when omissions are discovered). Each task description must be a one-line summary (max 60 chars), no newlines or markdown" +
|
|
717
|
+
"\n- update_tasks: Batch update task statuses (completed requires evidence, cancelled does not block goal completion)" +
|
|
718
|
+
"\n- list_tasks: View progress and remaining budget" +
|
|
719
|
+
"\n- complete_goal: Mark the objective as achieved (all tasks must be completed + evidence)" +
|
|
720
|
+
"\n- cancel_goal: Cancel the current goal (use when user wants to exit/stop)" +
|
|
721
|
+
"\n- report_blocked: Report being blocked (use when encountering unsolvable issues)" +
|
|
722
|
+
"\n- add_subtasks: Add subtasks to a specified task (params: taskId, texts[]). Use this instead of todo tool in Goal mode" +
|
|
723
|
+
"\n- update_subtasks: Batch update subtask statuses (params: taskId, subUpdates[])" +
|
|
724
|
+
"\n- delete_subtasks: Delete subtasks from a specified task (params: taskId, subIds[])",
|
|
725
|
+
promptSnippet: "Manage task list, completion status, and exit for /goal mode",
|
|
731
726
|
promptGuidelines: [
|
|
732
|
-
"[
|
|
733
|
-
"[
|
|
734
|
-
"[
|
|
735
|
-
"[
|
|
736
|
-
"[
|
|
737
|
-
"[
|
|
738
|
-
"[
|
|
739
|
-
"[
|
|
740
|
-
"[
|
|
741
|
-
"[
|
|
742
|
-
"[
|
|
743
|
-
"[
|
|
744
|
-
"[
|
|
727
|
+
"[Workflow] After receiving the objective, the first step must be create_tasks to decompose. Do not re-call if task list already exists",
|
|
728
|
+
"[Format] Each task description must be a one-line summary, max 60 chars. No newlines, markdown, or detailed parameter lists — those go in execution phase. Example: 'Fix hook-registry dedup logic' not 'Fix hook-registry dedup + transport-execute enhancementConfig guard + failover-loop ...'",
|
|
729
|
+
"[Append] When discovering omissions during execution, use add_tasks to append — do not re-call create_tasks",
|
|
730
|
+
"[Completion] After completing a task, call update_tasks with status=completed and provide evidence (e.g. 'test X passed', 'file F created')",
|
|
731
|
+
"[Goal completion] Only call complete_goal when all tasks are completed with overall evidence",
|
|
732
|
+
"[Exit] When user says 'stop', 'exit', 'cancel', '不用了', '结束', etc. indicating they don't want to continue, immediately call cancel_goal — do not guide them through complete_goal",
|
|
733
|
+
"[Blocked] When encountering unsolvable technical issues, call report_blocked with the reason",
|
|
734
|
+
"[Progress] Use list_tasks anytime to check remaining tasks and budget",
|
|
735
|
+
"[Cancel] To cancel a task, use update_tasks with status=cancelled. Cancelled tasks do not block goal completion",
|
|
736
|
+
"[Forbidden] Do not mark tasks as completed without evidence, and do not call complete_goal without evidence",
|
|
737
|
+
"[Forbidden] Do not force task completion when the user explicitly wants to exit — call cancel_goal directly",
|
|
738
|
+
"[Forbidden] Do not re-call create_tasks to overwrite existing incomplete tasks — use add_tasks to append",
|
|
739
|
+
"[Subtask] For fine-grained step tracking in Goal mode, use add_subtasks — do not use the todo tool",
|
|
745
740
|
],
|
|
746
741
|
parameters: GoalManagerParams,
|
|
747
742
|
|
|
@@ -777,7 +772,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
777
772
|
}
|
|
778
773
|
const tasks = details.tasks;
|
|
779
774
|
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
780
|
-
const summary = theme.fg("success", `✓ ${completed}/${tasks.length}
|
|
775
|
+
const summary = theme.fg("success", `✓ ${completed}/${tasks.length} completed`);
|
|
781
776
|
if (!expanded || tasks.length === 0) {
|
|
782
777
|
return new Text(summary, 0, 0);
|
|
783
778
|
}
|
|
@@ -816,7 +811,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
816
811
|
|
|
817
812
|
pi.registerCommand("goal", {
|
|
818
813
|
description:
|
|
819
|
-
"
|
|
814
|
+
"Goal-driven mode: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
|
|
820
815
|
handler: async (args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
821
816
|
await handleGoalCommand(pi, session, args, ctx);
|
|
822
817
|
},
|
|
@@ -890,9 +885,9 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
890
885
|
pi.registerMessageRenderer(customType, (message: any, _options: any, theme: Theme) => {
|
|
891
886
|
const prefix =
|
|
892
887
|
message.customType === "goal-context-exceeded"
|
|
893
|
-
? theme.fg("error", "[GOAL
|
|
888
|
+
? theme.fg("error", "[GOAL Budget] ")
|
|
894
889
|
: message.customType === "goal-staleness-reminder"
|
|
895
|
-
? theme.fg("warning", "[GOAL
|
|
890
|
+
? theme.fg("warning", "[GOAL Reminder] ")
|
|
896
891
|
: theme.fg("accent", "[GOAL] ");
|
|
897
892
|
const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
898
893
|
return new Text(prefix + theme.fg("dim", content), 0, 0);
|
package/src/templates.ts
CHANGED
|
@@ -33,20 +33,20 @@ export function continuationPrompt(state: GoalRuntimeState): string {
|
|
|
33
33
|
|
|
34
34
|
// Budget info (single line, Codex style)
|
|
35
35
|
const budgetLine = formatBudgetLine(state);
|
|
36
|
-
const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns}
|
|
36
|
+
const stallLine = state.stallCount > 0 ? `\nStall: ${state.stallCount}/${state.budget.maxStallTurns} turns stalled` : "";
|
|
37
37
|
|
|
38
38
|
// Task summary (only IDs, not full descriptions — descriptions in before_agent_start)
|
|
39
39
|
const taskLine = total > 0
|
|
40
|
-
? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (
|
|
41
|
-
: "Tasks:
|
|
40
|
+
? `Tasks: ${completedCount}/${total}${incomplete.length > 0 ? ` (remaining: ${incomplete.map(t => `#${t.id}`).join(",")})` : " ✓"}`
|
|
41
|
+
: "Tasks: Not created. Call create_tasks immediately.";
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
`<goal_context>\n` +
|
|
45
|
-
`[GOAL] Turn ${state.
|
|
45
|
+
`[GOAL] Turn ${state.currentTurnIndex}/${state.budget.maxTurns}${budgetLine}${stallLine}\n` +
|
|
46
46
|
`<objective>${objective}</objective>\n` +
|
|
47
47
|
`${taskLine}\n` +
|
|
48
|
-
`Rules: create_tasks→update_tasks(evidence)→complete_goal(evidence). blocked→report_blocked(reason). subtask: add_subtasks/update_subtasks (
|
|
49
|
-
`Audit:
|
|
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
50
|
`</goal_context>`
|
|
51
51
|
);
|
|
52
52
|
}
|
|
@@ -62,24 +62,24 @@ export function budgetLimitPrompt(state: GoalRuntimeState, limitType: "token" |
|
|
|
62
62
|
const incomplete = getIncompleteTasks(state.tasks);
|
|
63
63
|
const incompleteSummary =
|
|
64
64
|
incomplete.length > 0
|
|
65
|
-
?
|
|
66
|
-
: "
|
|
65
|
+
? `Incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}`
|
|
66
|
+
: "All tasks completed.";
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
69
|
`<goal_context>\n` +
|
|
70
|
-
`[GOAL — ${limitType === "token" ? "TOKEN
|
|
70
|
+
`[GOAL — ${limitType === "token" ? "TOKEN budget" : "time budget"} almost exhausted]\n\n` +
|
|
71
71
|
`<objective>\n${objective}\n</objective>\n\n` +
|
|
72
|
-
|
|
72
|
+
`Current progress: ${completedCount}/${total} tasks completed\n` +
|
|
73
73
|
`${incompleteSummary}\n` +
|
|
74
74
|
(limitType === "token"
|
|
75
|
-
? `
|
|
76
|
-
:
|
|
77
|
-
`\n
|
|
78
|
-
`1.
|
|
79
|
-
`2.
|
|
80
|
-
`3.
|
|
81
|
-
`4.
|
|
82
|
-
|
|
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
83
|
`</goal_context>`
|
|
84
84
|
);
|
|
85
85
|
}
|
|
@@ -92,14 +92,14 @@ export function objectiveUpdatedPrompt(state: GoalRuntimeState, oldObjective: st
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
`<goal_context>\n` +
|
|
95
|
-
`[GOAL —
|
|
96
|
-
|
|
95
|
+
`[GOAL — Objective updated]\n\n` +
|
|
96
|
+
`Previous objective: ${escapedOld}\n` +
|
|
97
97
|
`<untrusted_objective>\n${newObjective}\n</untrusted_objective>\n\n` +
|
|
98
|
-
|
|
99
|
-
`1.
|
|
100
|
-
`2.
|
|
101
|
-
`3.
|
|
102
|
-
`4.
|
|
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
103
|
`</goal_context>`
|
|
104
104
|
);
|
|
105
105
|
}
|
|
@@ -114,17 +114,17 @@ export function contextInjectionPrompt(state: GoalRuntimeState): string {
|
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
116
|
`<goal_context>\n` +
|
|
117
|
-
`[GOAL
|
|
117
|
+
`[GOAL mode activated]\n\n` +
|
|
118
118
|
`<objective>\n${objective}\n</objective>\n` +
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`1.
|
|
124
|
-
`2.
|
|
125
|
-
`3.
|
|
126
|
-
`4.
|
|
127
|
-
`5. Goal
|
|
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
128
|
`</goal_context>`
|
|
129
129
|
);
|
|
130
130
|
}
|
|
@@ -144,23 +144,23 @@ export function stalenessReminderPrompt(
|
|
|
144
144
|
const lines: string[] = [];
|
|
145
145
|
|
|
146
146
|
lines.push("<goal_context>");
|
|
147
|
-
lines.push("[GOAL
|
|
147
|
+
lines.push("[GOAL reminder — tasks stalled]\n");
|
|
148
148
|
|
|
149
149
|
if (allTerminal) {
|
|
150
|
-
lines.push("
|
|
150
|
+
lines.push("All tasks completed but goal_manager is still open. Call complete_goal or cancel_goal.");
|
|
151
151
|
} else {
|
|
152
|
-
lines.push(
|
|
152
|
+
lines.push(`The following tasks have exceeded ${TASK_STALL_TURN_THRESHOLD} turns without update:\n`);
|
|
153
153
|
for (const item of staleTasks) {
|
|
154
|
-
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns}
|
|
154
|
+
lines.push(` #${item.task.id}: ${item.task.description} (${item.staleTurns} turns idle)`);
|
|
155
155
|
for (const s of item.staleSubtasks) {
|
|
156
|
-
lines.push(` - ${s.text} (${s.staleTurns}
|
|
156
|
+
lines.push(` - ${s.text} (${s.staleTurns} turns)`);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
lines.push("\
|
|
159
|
+
lines.push("\nCheck these tasks — call update_tasks to report progress or cancel tasks that are no longer needed.");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
lines.push(`\
|
|
163
|
-
lines.push(`Turn: ${state.
|
|
162
|
+
lines.push(`\nObjective: ${objective}`);
|
|
163
|
+
lines.push(`Turn: ${state.currentTurnIndex}/${state.budget.maxTurns}`);
|
|
164
164
|
lines.push("</goal_context>");
|
|
165
165
|
|
|
166
166
|
return lines.join("\n");
|
|
@@ -177,7 +177,7 @@ function formatBudgetInfo(state: GoalRuntimeState): string {
|
|
|
177
177
|
if (state.budget.timeBudgetMinutes) {
|
|
178
178
|
const elapsed = getElapsedTimeSeconds(state);
|
|
179
179
|
const pct = Math.round((elapsed / (state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE)) * PERCENT_FACTOR);
|
|
180
|
-
parts.push(
|
|
180
|
+
parts.push(`Time: ${pct}%`);
|
|
181
181
|
}
|
|
182
182
|
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
183
183
|
}
|
|
@@ -197,13 +197,13 @@ function formatBudgetLine(state: GoalRuntimeState): string {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
export function formatTaskList(tasks: GoalTask[]): string {
|
|
200
|
-
if (tasks.length === 0) return "
|
|
200
|
+
if (tasks.length === 0) return "No tasks yet.";
|
|
201
201
|
const completed = tasks.filter(t => t.status === "completed");
|
|
202
202
|
const active = tasks.filter(t => t.status === "in_progress" || t.status === "pending");
|
|
203
203
|
const cancelled = tasks.filter(t => t.status === "cancelled");
|
|
204
204
|
const lines: string[] = [];
|
|
205
205
|
if (active.length > 0) {
|
|
206
|
-
lines.push(
|
|
206
|
+
lines.push(`In progress / Pending (${active.length}):`);
|
|
207
207
|
for (const t of active) {
|
|
208
208
|
const icon = t.status === "in_progress" ? "●" : "☐";
|
|
209
209
|
lines.push(` ${icon} #${t.id}: ${t.description}`);
|
|
@@ -216,17 +216,17 @@ export function formatTaskList(tasks: GoalTask[]): string {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
if (completed.length > 0) {
|
|
219
|
-
lines.push(
|
|
219
|
+
lines.push(`Completed (${completed.length}):`);
|
|
220
220
|
for (const t of completed) {
|
|
221
221
|
const evidence = t.evidence ? ` — ${t.evidence}` : "";
|
|
222
222
|
lines.push(` ✓ #${t.id}: ${t.description}${evidence}`);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
if (cancelled.length > 0) {
|
|
226
|
-
lines.push(
|
|
226
|
+
lines.push(`Cancelled (${cancelled.length}):`);
|
|
227
227
|
for (const t of cancelled) lines.push(` ✗ #${t.id}: ${t.description}`);
|
|
228
228
|
}
|
|
229
|
-
const summary = `${completed.length}/${tasks.length}
|
|
229
|
+
const summary = `${completed.length}/${tasks.length} completed` + (cancelled.length > 0 ? `, ${cancelled.length} cancelled` : "");
|
|
230
230
|
lines.push(summary);
|
|
231
231
|
return lines.join("\n");
|
|
232
232
|
}
|
package/src/tool-handler.ts
CHANGED
|
@@ -61,19 +61,19 @@ export const GoalManagerParams = Type.Object({
|
|
|
61
61
|
"update_subtasks",
|
|
62
62
|
"delete_subtasks",
|
|
63
63
|
] as const),
|
|
64
|
-
tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions.
|
|
64
|
+
tasks: Type.Optional(Type.Array(Type.String(), { description: "Task descriptions. Each must be a one-line summary (max 60 chars), no newlines or markdown" })),
|
|
65
65
|
updates: Type.Optional(Type.Array(Type.Object({
|
|
66
66
|
taskId: Type.Number(),
|
|
67
67
|
status: StringEnum(GOAL_TASK_STATUSES),
|
|
68
68
|
evidence: Type.Optional(Type.String()),
|
|
69
69
|
}))),
|
|
70
|
-
taskId: Type.Optional(Type.Number({ description: "Task ID
|
|
71
|
-
texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask
|
|
70
|
+
taskId: Type.Optional(Type.Number({ description: "Task ID (required for subtask operations)" })),
|
|
71
|
+
texts: Type.Optional(Type.Array(Type.String(), { description: "Subtask text list (for add_subtasks)" })),
|
|
72
72
|
subUpdates: Type.Optional(Type.Array(Type.Object({
|
|
73
73
|
subId: Type.Number(),
|
|
74
74
|
status: StringEnum(SUBTASK_STATUSES),
|
|
75
75
|
}))),
|
|
76
|
-
subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID
|
|
76
|
+
subIds: Type.Optional(Type.Array(Type.Number(), { description: "Subtask ID list (for delete_subtasks)" })),
|
|
77
77
|
evidence: Type.Optional(Type.String({ description: "Evidence for completion (required for complete_goal)" })),
|
|
78
78
|
reason: Type.Optional(Type.String({ description: "Reason for being blocked (required for report_blocked)" })),
|
|
79
79
|
cancelReason: Type.Optional(Type.String({ description: "Why the user wants to cancel (required for cancel_goal)" })),
|
|
@@ -160,12 +160,12 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
160
160
|
const budgetInfo: string[] = [];
|
|
161
161
|
if (state.budget.tokenBudget) {
|
|
162
162
|
const remaining = Math.max(state.budget.tokenBudget - state.tokensUsed, 0);
|
|
163
|
-
budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (
|
|
163
|
+
budgetInfo.push(`Token: ${state.tokensUsed}/${state.budget.tokenBudget} (${remaining} remaining)`);
|
|
164
164
|
}
|
|
165
165
|
if (state.budget.timeBudgetMinutes) {
|
|
166
166
|
const elapsed = getElapsedTimeSeconds(state);
|
|
167
167
|
const remaining = Math.max(state.budget.timeBudgetMinutes * SECONDS_PER_MINUTE - elapsed, 0);
|
|
168
|
-
budgetInfo.push(
|
|
168
|
+
budgetInfo.push(`Time: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m (${Math.floor(remaining / SECONDS_PER_MINUTE)}m remaining)`);
|
|
169
169
|
}
|
|
170
170
|
const suffix = budgetInfo.length > 0 ? `\n\n[Budget] ${budgetInfo.join(" | ")}` : "";
|
|
171
171
|
return {
|
|
@@ -177,7 +177,7 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
177
177
|
status: state.status,
|
|
178
178
|
_render: {
|
|
179
179
|
type: "task-list" as const,
|
|
180
|
-
summary: `${getCompletedCount(state.tasks)}/${state.tasks.length}
|
|
180
|
+
summary: `${getCompletedCount(state.tasks)}/${state.tasks.length} completed`,
|
|
181
181
|
data: {
|
|
182
182
|
items: state.tasks.map((t) => ({
|
|
183
183
|
id: t.id,
|
|
@@ -192,8 +192,8 @@ export function makeGoalResult(session: GoalSession, text: string) {
|
|
|
192
192
|
})),
|
|
193
193
|
meta: {
|
|
194
194
|
...(state.budget.tokenBudget ? { "Token": `${state.tokensUsed}/${state.budget.tokenBudget}` } : {}),
|
|
195
|
-
...(state.budget.timeBudgetMinutes ? { "
|
|
196
|
-
"
|
|
195
|
+
...(state.budget.timeBudgetMinutes ? { "Time": `${Math.floor(getElapsedTimeSeconds(state) / SECONDS_PER_MINUTE)}m/${state.budget.timeBudgetMinutes}m` } : {}),
|
|
196
|
+
"Turn": `${state.currentTurnIndex}/${state.budget.maxTurns}`,
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
199
|
},
|
|
@@ -222,7 +222,7 @@ export async function executeGoalAction(
|
|
|
222
222
|
) {
|
|
223
223
|
const state = session.state;
|
|
224
224
|
if (!state) {
|
|
225
|
-
throw new Error("Goal
|
|
225
|
+
throw new Error("Goal mode not active. Use /goal <objective> to start.");
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
switch (params.action) {
|
|
@@ -233,8 +233,7 @@ export async function executeGoalAction(
|
|
|
233
233
|
const existingIncomplete = getIncompleteTasks(state.tasks);
|
|
234
234
|
if (state.tasks.length > 0 && existingIncomplete.length > 0) {
|
|
235
235
|
throw new Error(
|
|
236
|
-
|
|
237
|
-
`如需追加任务请用 add_tasks,如需全部重新规划请用 /goal update。`,
|
|
236
|
+
`Already has ${state.tasks.length} tasks (${existingIncomplete.length} incomplete). Use add_tasks to append, or /goal update to re-plan.`,
|
|
238
237
|
);
|
|
239
238
|
}
|
|
240
239
|
state.tasks = params.tasks.map((desc: string, i: number) => ({
|
|
@@ -245,7 +244,7 @@ export async function executeGoalAction(
|
|
|
245
244
|
}));
|
|
246
245
|
persistGoalState(pi, session, ctx);
|
|
247
246
|
return makeGoalResult(session,
|
|
248
|
-
|
|
247
|
+
`Created ${state.tasks.length} tasks:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
|
|
249
248
|
);
|
|
250
249
|
}
|
|
251
250
|
|
|
@@ -265,7 +264,7 @@ export async function executeGoalAction(
|
|
|
265
264
|
state.tasks.push(...newTasks);
|
|
266
265
|
persistGoalState(pi, session, ctx);
|
|
267
266
|
return makeGoalResult(session,
|
|
268
|
-
|
|
267
|
+
`Appended ${newTasks.length} tasks:\n${newTasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
|
|
269
268
|
);
|
|
270
269
|
}
|
|
271
270
|
|
|
@@ -276,7 +275,7 @@ export async function executeGoalAction(
|
|
|
276
275
|
const taskIds = params.updates.map((u: { taskId: number; status: string; evidence?: string }) => u.taskId);
|
|
277
276
|
const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
|
|
278
277
|
if (duplicateIds.length > 0) {
|
|
279
|
-
throw new Error(
|
|
278
|
+
throw new Error(`Duplicate taskIds: ${[...new Set(duplicateIds)].join(", ")}`);
|
|
280
279
|
}
|
|
281
280
|
for (const u of params.updates) {
|
|
282
281
|
const task = state.tasks.find((t) => t.id === u.taskId);
|
|
@@ -284,10 +283,10 @@ export async function executeGoalAction(
|
|
|
284
283
|
throw new Error(`Task #${u.taskId} not found`);
|
|
285
284
|
}
|
|
286
285
|
if (isTerminalTaskStatus(task.status)) {
|
|
287
|
-
throw new Error(`Task #${task.id}
|
|
286
|
+
throw new Error(`Task #${task.id} already in terminal state (${task.status}), cannot be changed`);
|
|
288
287
|
}
|
|
289
288
|
if (u.status === "completed" && (!u.evidence || u.evidence.trim() === "")) {
|
|
290
|
-
throw new Error(`Task #${task.id}: completed
|
|
289
|
+
throw new Error(`Task #${task.id}: completed requires evidence`);
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
292
|
const results: string[] = [];
|
|
@@ -305,7 +304,7 @@ export async function executeGoalAction(
|
|
|
305
304
|
}
|
|
306
305
|
}
|
|
307
306
|
persistGoalState(pi, session, ctx);
|
|
308
|
-
return makeGoalResult(session,
|
|
307
|
+
return makeGoalResult(session, `Updated ${results.length} tasks:\n${results.join("\n")}`);
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
case "list_tasks": {
|
|
@@ -314,54 +313,53 @@ export async function executeGoalAction(
|
|
|
314
313
|
|
|
315
314
|
case "complete_goal": {
|
|
316
315
|
if (!params.evidence || params.evidence.trim() === "") {
|
|
317
|
-
throw new Error("complete_goal requires evidence —
|
|
316
|
+
throw new Error("complete_goal requires evidence — provide concrete proof that the objective has been achieved");
|
|
318
317
|
}
|
|
319
318
|
if (state.tasks.length === 0) {
|
|
320
|
-
throw new Error("
|
|
319
|
+
throw new Error("Create a task list with create_tasks before completing the goal.");
|
|
321
320
|
}
|
|
322
321
|
const incomplete = getIncompleteTasks(state.tasks);
|
|
323
322
|
if (incomplete.length > 0) {
|
|
324
323
|
throw new Error(
|
|
325
|
-
|
|
326
|
-
`请先完成这些任务或提供理由说明为什么它们不需要完成。`,
|
|
324
|
+
`${incomplete.length} tasks still incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}. Complete them first or explain why they don't need completion.`,
|
|
327
325
|
);
|
|
328
326
|
}
|
|
329
327
|
const completedCount = getCompletedCount(state.tasks);
|
|
330
328
|
if (completedCount === 0) {
|
|
331
|
-
throw new Error("
|
|
329
|
+
throw new Error("At least one task must be completed. All-cancelled does not count.");
|
|
332
330
|
}
|
|
333
331
|
state.status = transitionStatus(state.status, "complete");
|
|
334
332
|
state.completedAtTurnIndex = state.currentTurnIndex;
|
|
335
333
|
writeGoalHistoryEntry(pi, session);
|
|
336
334
|
persistGoalState(pi, session, ctx);
|
|
337
335
|
const budgetReport: string[] = [];
|
|
338
|
-
budgetReport.push(
|
|
339
|
-
budgetReport.push(
|
|
336
|
+
budgetReport.push(`Total turns: ${state.currentTurnIndex}`);
|
|
337
|
+
budgetReport.push(`Tasks completed: ${getCompletedCount(state.tasks)}/${state.tasks.length}`);
|
|
340
338
|
if (state.budget.tokenBudget) {
|
|
341
|
-
budgetReport.push(`Token
|
|
339
|
+
budgetReport.push(`Token usage: ${state.tokensUsed}/${state.budget.tokenBudget}`);
|
|
342
340
|
}
|
|
343
341
|
const elapsed = getElapsedTimeSeconds(state);
|
|
344
|
-
budgetReport.push(
|
|
342
|
+
budgetReport.push(`Duration: ${Math.floor(elapsed / SECONDS_PER_MINUTE)}m${Math.floor(elapsed % SECONDS_PER_MINUTE)}s`);
|
|
345
343
|
return makeGoalResult(session,
|
|
346
|
-
|
|
344
|
+
`Objective completed!\nEvidence: ${params.evidence}\n\n--- Budget Report ---\n${budgetReport.join("\n")}`,
|
|
347
345
|
);
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
case "report_blocked": {
|
|
351
349
|
if (!params.reason || params.reason.trim() === "") {
|
|
352
|
-
throw new Error("report_blocked requires reason —
|
|
350
|
+
throw new Error("report_blocked requires reason — describe what is blocking you");
|
|
353
351
|
}
|
|
354
352
|
state.lastBlockerReason = params.reason;
|
|
355
353
|
state.status = transitionStatus(state.status, "blocked");
|
|
356
354
|
persistGoalState(pi, session, ctx);
|
|
357
|
-
return makeGoalResult(session,
|
|
355
|
+
return makeGoalResult(session, `Blocked reported. Reason: ${params.reason}`);
|
|
358
356
|
}
|
|
359
357
|
|
|
360
358
|
case "cancel_goal": {
|
|
361
359
|
if (isTerminalStatus(state.status)) {
|
|
362
|
-
throw new Error(`Goal
|
|
360
|
+
throw new Error(`Goal is already in terminal state (${state.status}).`);
|
|
363
361
|
}
|
|
364
|
-
const reason = params.cancelReason ?? "
|
|
362
|
+
const reason = params.cancelReason ?? "User requested cancellation";
|
|
365
363
|
const goalId = state.goalId;
|
|
366
364
|
state.status = "cancelled";
|
|
367
365
|
state.completedAtTurnIndex = state.currentTurnIndex;
|
|
@@ -369,7 +367,7 @@ export async function executeGoalAction(
|
|
|
369
367
|
persistGoalState(pi, session, ctx);
|
|
370
368
|
clearGoalSession(session, ctx);
|
|
371
369
|
return {
|
|
372
|
-
content: [{ type: "text" as const, text: `Goal
|
|
370
|
+
content: [{ type: "text" as const, text: `Goal cancelled: ${reason}` }],
|
|
373
371
|
details: {
|
|
374
372
|
action: "cancel",
|
|
375
373
|
tasks: [],
|
|
@@ -377,7 +375,7 @@ export async function executeGoalAction(
|
|
|
377
375
|
status: "cancelled",
|
|
378
376
|
_render: {
|
|
379
377
|
type: "task-list" as const,
|
|
380
|
-
summary: "
|
|
378
|
+
summary: "Cancelled",
|
|
381
379
|
data: { items: [], meta: {} },
|
|
382
380
|
},
|
|
383
381
|
} satisfies GoalManagerDetails,
|
|
@@ -396,13 +394,13 @@ export async function executeGoalAction(
|
|
|
396
394
|
throw new Error(`Task #${params.taskId} not found`);
|
|
397
395
|
}
|
|
398
396
|
if (isTerminalTaskStatus(parentTask.status)) {
|
|
399
|
-
throw new Error(`Task #${parentTask.id}
|
|
397
|
+
throw new Error(`Task #${parentTask.id} already in terminal state (${parentTask.status}), cannot add subtask`);
|
|
400
398
|
}
|
|
401
399
|
const subtasks = parentTask.subtasks ?? [];
|
|
402
400
|
const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
|
|
403
401
|
const trimmed = params.texts.map((t: string) => t.trim()).filter((t: string) => t.length > 0);
|
|
404
402
|
if (trimmed.length === 0) {
|
|
405
|
-
throw new Error("texts
|
|
403
|
+
throw new Error("texts requires at least one non-empty string");
|
|
406
404
|
}
|
|
407
405
|
const newSubtasks: Subtask[] = trimmed.map((text: string, i: number) => ({
|
|
408
406
|
id: startId + i,
|
|
@@ -413,7 +411,7 @@ export async function executeGoalAction(
|
|
|
413
411
|
parentTask.subtasks = [...subtasks, ...newSubtasks];
|
|
414
412
|
persistGoalState(pi, session, ctx);
|
|
415
413
|
return makeGoalResult(session,
|
|
416
|
-
|
|
414
|
+
`Added ${newSubtasks.length} subtasks to Task #${parentTask.id}:\n` +
|
|
417
415
|
newSubtasks.map((s) => ` - #${parentTask.id}.${s.id}: ${s.text}`).join("\n"),
|
|
418
416
|
);
|
|
419
417
|
}
|
|
@@ -430,7 +428,7 @@ export async function executeGoalAction(
|
|
|
430
428
|
throw new Error(`Task #${params.taskId} not found`);
|
|
431
429
|
}
|
|
432
430
|
if (!targetTask.subtasks || targetTask.subtasks.length === 0) {
|
|
433
|
-
throw new Error(`Task #${params.taskId}
|
|
431
|
+
throw new Error(`Task #${params.taskId} has no subtasks`);
|
|
434
432
|
}
|
|
435
433
|
const results: string[] = [];
|
|
436
434
|
for (const u of params.subUpdates) {
|
|
@@ -439,7 +437,7 @@ export async function executeGoalAction(
|
|
|
439
437
|
throw new Error(`Subtask #${params.taskId}.${u.subId} not found`);
|
|
440
438
|
}
|
|
441
439
|
if (sub.status === "completed") {
|
|
442
|
-
throw new Error(`Subtask #${params.taskId}.${sub.id}
|
|
440
|
+
throw new Error(`Subtask #${params.taskId}.${sub.id} already completed, cannot be changed`);
|
|
443
441
|
}
|
|
444
442
|
const prev = sub.status;
|
|
445
443
|
sub.status = u.status;
|
|
@@ -448,7 +446,7 @@ export async function executeGoalAction(
|
|
|
448
446
|
}
|
|
449
447
|
persistGoalState(pi, session, ctx);
|
|
450
448
|
return makeGoalResult(session,
|
|
451
|
-
|
|
449
|
+
`Updated ${results.length} subtasks:\n${results.join("\n")}`,
|
|
452
450
|
);
|
|
453
451
|
}
|
|
454
452
|
|
|
@@ -464,7 +462,7 @@ export async function executeGoalAction(
|
|
|
464
462
|
throw new Error(`Task #${params.taskId} not found`);
|
|
465
463
|
}
|
|
466
464
|
if (!delTask.subtasks || delTask.subtasks.length === 0) {
|
|
467
|
-
throw new Error(`Task #${params.taskId}
|
|
465
|
+
throw new Error(`Task #${params.taskId} has no subtasks`);
|
|
468
466
|
}
|
|
469
467
|
const uniqueIds = [...new Set(params.subIds)];
|
|
470
468
|
const missing = uniqueIds.filter((id) => !delTask.subtasks!.some((s) => s.id === id));
|
|
@@ -477,7 +475,7 @@ export async function executeGoalAction(
|
|
|
477
475
|
}
|
|
478
476
|
persistGoalState(pi, session, ctx);
|
|
479
477
|
return makeGoalResult(session,
|
|
480
|
-
|
|
478
|
+
`Deleted ${uniqueIds.length} subtasks, Task #${params.taskId} has ${delTask.subtasks?.length ?? 0} remaining`,
|
|
481
479
|
);
|
|
482
480
|
}
|
|
483
481
|
|
package/src/widget.ts
CHANGED
|
@@ -39,13 +39,13 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
|
|
|
39
39
|
const completedCount = getCompletedCount(state.tasks);
|
|
40
40
|
const total = state.tasks.length;
|
|
41
41
|
|
|
42
|
-
let text = th.fg("accent", `◆ Goal`) + th.fg("muted", ` ${state.
|
|
42
|
+
let text = th.fg("accent", `◆ Goal`) + th.fg("muted", ` ${state.currentTurnIndex}/${state.budget.maxTurns}`);
|
|
43
43
|
|
|
44
44
|
if (total > 0) {
|
|
45
|
-
text += th.fg("muted", ` | ${completedCount}/${total}
|
|
45
|
+
text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
|
|
46
46
|
const cancelledCount = state.tasks.filter(t => t.status === "cancelled").length;
|
|
47
47
|
if (cancelledCount > 0) {
|
|
48
|
-
text += th.fg("dim", `, ${cancelledCount}
|
|
48
|
+
text += th.fg("dim", `, ${cancelledCount} cancelled`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -60,25 +60,25 @@ export function renderStatusLine(state: GoalRuntimeState, th: ThemeLike): string
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (state.stallCount > 0) {
|
|
63
|
-
text += th.fg("warning", ` | ⚠ ${state.stallCount}
|
|
63
|
+
text += th.fg("warning", ` | ⚠ ${state.stallCount} turns stalled`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Status suffix
|
|
67
67
|
switch (state.status) {
|
|
68
68
|
case "paused":
|
|
69
|
-
text += th.fg("warning", " | ⏸
|
|
69
|
+
text += th.fg("warning", " | ⏸ Paused");
|
|
70
70
|
break;
|
|
71
71
|
case "blocked":
|
|
72
|
-
text += th.fg("error", " | ⊘
|
|
72
|
+
text += th.fg("error", " | ⊘ Blocked");
|
|
73
73
|
break;
|
|
74
74
|
case "complete":
|
|
75
|
-
text += th.fg("success", " | ✓
|
|
75
|
+
text += th.fg("success", " | ✓ Completed");
|
|
76
76
|
break;
|
|
77
77
|
case "budget_limited":
|
|
78
|
-
text += th.fg("error", " | ⊗ Token
|
|
78
|
+
text += th.fg("error", " | ⊗ Token budget exhausted");
|
|
79
79
|
break;
|
|
80
80
|
case "time_limited":
|
|
81
|
-
text += th.fg("error", " | ⏱
|
|
81
|
+
text += th.fg("error", " | ⏱ Time budget exhausted");
|
|
82
82
|
break;
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -96,19 +96,19 @@ export function renderTerminalStatusLine(state: GoalRuntimeState, th: ThemeLike)
|
|
|
96
96
|
// 状态后缀
|
|
97
97
|
switch (state.status) {
|
|
98
98
|
case "complete":
|
|
99
|
-
text += th.fg("success", " ✓
|
|
99
|
+
text += th.fg("success", " ✓ Completed");
|
|
100
100
|
break;
|
|
101
101
|
case "budget_limited":
|
|
102
|
-
text += th.fg("error", " ⊗ Token
|
|
102
|
+
text += th.fg("error", " ⊗ Token budget exhausted");
|
|
103
103
|
break;
|
|
104
104
|
case "time_limited":
|
|
105
|
-
text += th.fg("error", " ⏱
|
|
105
|
+
text += th.fg("error", " ⏱ Time budget exhausted");
|
|
106
106
|
break;
|
|
107
107
|
default:
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
text += th.fg("muted", ` | ${completedCount}/${total}
|
|
111
|
+
text += th.fg("muted", ` | ${completedCount}/${total} tasks`);
|
|
112
112
|
|
|
113
113
|
// 预算摘要
|
|
114
114
|
if (state.budget.tokenBudget && state.budget.tokenBudget > 0) {
|
|
@@ -137,11 +137,11 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
|
|
|
137
137
|
const objDisplay = objSingleLine.length > OBJECTIVE_DISPLAY_LIMIT
|
|
138
138
|
? objSingleLine.slice(0, OBJECTIVE_TRUNCATE_KEEP) + "..."
|
|
139
139
|
: objSingleLine;
|
|
140
|
-
lines.push(th.fg("dim",
|
|
140
|
+
lines.push(th.fg("dim", `Objective: ${objDisplay}`));
|
|
141
141
|
|
|
142
142
|
// Task list
|
|
143
143
|
if (total === 0) {
|
|
144
|
-
lines.push(th.fg("dim", "
|
|
144
|
+
lines.push(th.fg("dim", " Waiting for task list creation..."));
|
|
145
145
|
} else {
|
|
146
146
|
for (const t of state.tasks) {
|
|
147
147
|
const desc = toSingleLine(t.description);
|
|
@@ -178,7 +178,7 @@ export function renderWidgetLines(state: GoalRuntimeState, th: ThemeLike): strin
|
|
|
178
178
|
const pct = getTimeUsagePercent(state) / PERCENT_FACTOR;
|
|
179
179
|
const elapsed = getElapsedTimeSeconds(state);
|
|
180
180
|
const mins = Math.floor(elapsed / SECONDS_PER_MINUTE);
|
|
181
|
-
lines.push(`
|
|
181
|
+
lines.push(` Time: ${renderProgressBar(pct)} ${mins}/${state.budget.timeBudgetMinutes}min`);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
return lines;
|