@signalridge/pi-goal 0.49.7
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +347 -0
- package/package.json +67 -0
- package/src/accounting.ts +102 -0
- package/src/command-registration.ts +142 -0
- package/src/command.ts +170 -0
- package/src/commands.ts +693 -0
- package/src/errors.ts +170 -0
- package/src/goal.ts +52 -0
- package/src/index.ts +1 -0
- package/src/lifecycle.ts +626 -0
- package/src/markers.ts +25 -0
- package/src/menu.ts +854 -0
- package/src/persistence.ts +296 -0
- package/src/prompts.ts +93 -0
- package/src/queue.ts +102 -0
- package/src/run-protocol.ts +355 -0
- package/src/runtime.ts +1205 -0
- package/src/safety.ts +78 -0
- package/src/settings-ui.ts +605 -0
- package/src/settings.ts +200 -0
- package/src/tool-policy.ts +178 -0
- package/src/tools.ts +334 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import { checkpointGoalActiveTime, currentTokenTotal, formatTokenCount } from "./accounting.js";
|
|
2
|
+
import { validateObjective } from "./command.js";
|
|
3
|
+
import { notifyTerminal, safeGoalMenuText } from "./errors.js";
|
|
4
|
+
import type { ActiveGoal } from "./persistence.js";
|
|
5
|
+
import { buildGoalPrompt, buildObjectiveUpdatedPrompt, buildResumePrompt } from "./prompts.js";
|
|
6
|
+
import {
|
|
7
|
+
activateQueuedGoal,
|
|
8
|
+
appendGoal,
|
|
9
|
+
createQueuedGoal,
|
|
10
|
+
dropLastGoal as dropLastQueuedGoal,
|
|
11
|
+
goalQueueIdentity,
|
|
12
|
+
prioritizeGoal as prioritizeQueuedGoal,
|
|
13
|
+
skipGoal as skipQueuedGoal,
|
|
14
|
+
} from "./queue.js";
|
|
15
|
+
import {
|
|
16
|
+
blocksStaleGoalToolCalls,
|
|
17
|
+
createGoal,
|
|
18
|
+
editedGoalStatus,
|
|
19
|
+
formatBudget,
|
|
20
|
+
formatError,
|
|
21
|
+
type GoalRuntime,
|
|
22
|
+
goalSummary,
|
|
23
|
+
hasPendingMessages,
|
|
24
|
+
isResumableGoalStatus,
|
|
25
|
+
nextGoalInstance,
|
|
26
|
+
queueGoalSafetyReset,
|
|
27
|
+
STATUS_KEY,
|
|
28
|
+
type StatusContext,
|
|
29
|
+
stoppedStatusLabel,
|
|
30
|
+
transitionGoal,
|
|
31
|
+
} from "./runtime.js";
|
|
32
|
+
|
|
33
|
+
// User-command mutations are kept separate from Pi event wiring. Every controller
|
|
34
|
+
// receives exactly one per-factory GoalRuntime, preserving session isolation.
|
|
35
|
+
export class GoalCommandController {
|
|
36
|
+
private readonly runtime: GoalRuntime;
|
|
37
|
+
|
|
38
|
+
constructor(runtime: GoalRuntime) {
|
|
39
|
+
this.runtime = runtime;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async startGoal(
|
|
43
|
+
objective: string,
|
|
44
|
+
tokenBudget: number | undefined,
|
|
45
|
+
ctx: StatusContext,
|
|
46
|
+
onActivated?: (goal: ActiveGoal) => void,
|
|
47
|
+
isActivationCurrent?: (goal: ActiveGoal) => boolean,
|
|
48
|
+
isRequestCurrent?: () => boolean,
|
|
49
|
+
) {
|
|
50
|
+
if (isRequestCurrent && !isRequestCurrent()) return;
|
|
51
|
+
const validationError = validateObjective(objective);
|
|
52
|
+
if (validationError) {
|
|
53
|
+
notifyTerminal(ctx.ui, validationError, "warning");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const existingGoal = this.runtime.activeGoal?.status !== "complete" ? this.runtime.activeGoal : undefined;
|
|
58
|
+
const existingQueuedGoals = [...this.runtime.queuedGoals];
|
|
59
|
+
const existingQueueIdentity = goalQueueIdentity(
|
|
60
|
+
this.runtime.activeGoal,
|
|
61
|
+
this.runtime.queuedGoals,
|
|
62
|
+
this.runtime.pendingQueueAction,
|
|
63
|
+
);
|
|
64
|
+
if (existingGoal) {
|
|
65
|
+
const queuedRemovalPreview =
|
|
66
|
+
existingQueuedGoals.length > 0
|
|
67
|
+
? `\n\nQueued goals also removed:\n${existingQueuedGoals
|
|
68
|
+
.map((goal, index) => `${index + 1}. ${safeGoalMenuText(goal.text, 4_000)}`)
|
|
69
|
+
.join("\n")}`
|
|
70
|
+
: "";
|
|
71
|
+
const shouldReplace = await ctx.ui.confirm(
|
|
72
|
+
"Replace goal?",
|
|
73
|
+
`Current goal: ${safeGoalMenuText(existingGoal.text, 4_000)}${queuedRemovalPreview}\n\nNew goal: ${safeGoalMenuText(objective, 4_000)}`,
|
|
74
|
+
);
|
|
75
|
+
if (!shouldReplace) {
|
|
76
|
+
notifyTerminal(ctx.ui, `Goal kept: ${existingGoal.text}`, "info");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (isRequestCurrent && !isRequestCurrent()) return;
|
|
80
|
+
if (
|
|
81
|
+
goalQueueIdentity(this.runtime.activeGoal, this.runtime.queuedGoals, this.runtime.pendingQueueAction) !==
|
|
82
|
+
existingQueueIdentity
|
|
83
|
+
) {
|
|
84
|
+
notifyTerminal(ctx.ui, "The goal queue changed while confirmation was open. Try again.", "warning");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unlock lazy visibility only for a real activation. In always mode, a
|
|
90
|
+
// missing tool means another policy or allowlist intentionally removed it.
|
|
91
|
+
if (isRequestCurrent && !isRequestCurrent()) return;
|
|
92
|
+
const goalToolVisibilityBeforeActivation = this.runtime.toolPolicy.snapshot();
|
|
93
|
+
try {
|
|
94
|
+
this.runtime.toolPolicy.prepareActivation(this.runtime.settings.toolVisibility, ctx);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
notifyTerminal(ctx.ui, `Cannot start /goal: ${formatError(error)}`, "error");
|
|
97
|
+
if (existingGoal?.status === "active") this.runtime.pauseGoalForUnavailableTools(ctx);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.runtime.cancelContinuationWork();
|
|
102
|
+
this.runtime.clearGoalRecovery();
|
|
103
|
+
this.runtime.clearBudgetWrapUp();
|
|
104
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
105
|
+
this.runtime.queuedGoals = [];
|
|
106
|
+
this.runtime.pendingQueueAction = undefined;
|
|
107
|
+
this.runtime.activeGoal = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
|
|
108
|
+
const startedGoal = this.runtime.activeGoal;
|
|
109
|
+
onActivated?.(startedGoal);
|
|
110
|
+
this.runtime.persistGoal(startedGoal);
|
|
111
|
+
if (this.runtime.activeGoal?.id !== startedGoal.id || this.runtime.activeGoal.status !== "active") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.runtime.updateStatus(ctx, startedGoal);
|
|
115
|
+
const sent = await this.runtime.sendOwnedGoalPrompt(
|
|
116
|
+
ctx,
|
|
117
|
+
startedGoal.id,
|
|
118
|
+
buildGoalPrompt(startedGoal),
|
|
119
|
+
true,
|
|
120
|
+
() => (isRequestCurrent?.() ?? true) && (isActivationCurrent?.(startedGoal) ?? true),
|
|
121
|
+
);
|
|
122
|
+
if (isActivationCurrent && !isActivationCurrent(startedGoal)) return;
|
|
123
|
+
if (!sent) {
|
|
124
|
+
let rolledBackStartedGoal = false;
|
|
125
|
+
if (this.runtime.activeGoal?.id === startedGoal.id) {
|
|
126
|
+
rolledBackStartedGoal = true;
|
|
127
|
+
if (existingGoal) {
|
|
128
|
+
this.runtime.queuedGoals = existingQueuedGoals;
|
|
129
|
+
this.runtime.recordGoalUsage(existingGoal, ctx);
|
|
130
|
+
if (existingGoal.status === "active") {
|
|
131
|
+
this.runtime.stopActiveGoal(ctx, {
|
|
132
|
+
kind: "activation_rollback",
|
|
133
|
+
expectedGoalId: startedGoal.id,
|
|
134
|
+
restoreGoal: existingGoal,
|
|
135
|
+
abortTurn: true,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
this.runtime.activeGoal = existingGoal;
|
|
139
|
+
if (blocksStaleGoalToolCalls(this.runtime.activeGoal.status)) {
|
|
140
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
141
|
+
} else {
|
|
142
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
143
|
+
}
|
|
144
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
145
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
this.runtime.clearActiveGoal(ctx);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (rolledBackStartedGoal) {
|
|
152
|
+
this.runtime.toolPolicy.restore(goalToolVisibilityBeforeActivation);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (this.runtime.activeGoal?.id !== startedGoal.id || this.runtime.activeGoal.status !== "active") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const automaticLimit = this.runtime.settings.continuationLimits.automaticTurns;
|
|
160
|
+
notifyTerminal(
|
|
161
|
+
ctx.ui,
|
|
162
|
+
`${existingGoal ? "Goal replaced" : "Goal started"}: ${objective}. ${
|
|
163
|
+
startedGoal.tokenBudget === undefined
|
|
164
|
+
? ""
|
|
165
|
+
: `Token budget: ${formatTokenCount(startedGoal.tokenBudget)} cumulative; the final model call may exceed it. `
|
|
166
|
+
}${
|
|
167
|
+
automaticLimit === null
|
|
168
|
+
? "Automatic work is Unlimited; tool loops may consume substantial tokens and provider cost. Open /goal to monitor."
|
|
169
|
+
: `Automatic work pauses after ${automaticLimit} responses; open /goal to monitor progress.`
|
|
170
|
+
}`,
|
|
171
|
+
automaticLimit === null ? "warning" : "info",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async addGoal(objective: string, tokenBudget: number | undefined, ctx: StatusContext) {
|
|
176
|
+
const validationError = validateObjective(objective);
|
|
177
|
+
if (validationError) {
|
|
178
|
+
notifyTerminal(ctx.ui, validationError, "warning");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!this.runtime.activeGoal) {
|
|
182
|
+
await this.startGoal(objective, tokenBudget, ctx);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.runtime.queuedGoals = appendGoal(this.runtime.queuedGoals, createQueuedGoal(objective, tokenBudget));
|
|
186
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
187
|
+
notifyTerminal(ctx.ui, `Goal added at position ${this.runtime.queuedGoals.length + 1}: ${objective}`, "info");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async prioritizeGoal(objective: string, tokenBudget: number | undefined, ctx: StatusContext) {
|
|
191
|
+
const validationError = validateObjective(objective);
|
|
192
|
+
if (validationError) {
|
|
193
|
+
notifyTerminal(ctx.ui, validationError, "warning");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!this.runtime.activeGoal) {
|
|
197
|
+
await this.startGoal(objective, tokenBudget, ctx);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.runtime.cancelContinuationWork();
|
|
201
|
+
this.runtime.pendingQueueAction = { kind: "prioritize", objective, tokenBudget };
|
|
202
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
203
|
+
if (ctx.isIdle?.() !== true || hasPendingMessages(ctx)) {
|
|
204
|
+
notifyTerminal(ctx.ui, `Priority goal queued until Pi settles: ${objective}`, "info");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await this.dispatchPendingQueueActionIfSettled(ctx);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
dropLastGoal(ctx: StatusContext) {
|
|
211
|
+
const currentGoal = this.runtime.activeGoal;
|
|
212
|
+
if (!currentGoal) {
|
|
213
|
+
notifyTerminal(ctx.ui, "No goals to drop.", "info");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const result = dropLastQueuedGoal(currentGoal, this.runtime.queuedGoals);
|
|
217
|
+
if (!result.goal) {
|
|
218
|
+
this.runtime.clearActiveGoal(ctx);
|
|
219
|
+
notifyTerminal(ctx.ui, `Goal dropped: ${result.removed?.text ?? currentGoal.text}`, "warning");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.runtime.queuedGoals = result.queue;
|
|
223
|
+
this.runtime.persistGoal(result.goal);
|
|
224
|
+
notifyTerminal(ctx.ui, `Goal dropped: ${result.removed?.text ?? "unknown goal"}`, "warning");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async skipGoal(ctx: StatusContext) {
|
|
228
|
+
const currentGoal = this.runtime.activeGoal;
|
|
229
|
+
if (!currentGoal) {
|
|
230
|
+
notifyTerminal(ctx.ui, "No goals to skip.", "info");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (this.runtime.queuedGoals.length === 0) {
|
|
234
|
+
this.runtime.clearActiveGoal(ctx);
|
|
235
|
+
notifyTerminal(ctx.ui, `Goal skipped: ${currentGoal.text}. No goals remain.`, "warning");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (currentGoal.status === "active") this.runtime.recordGoalUsage(currentGoal, ctx);
|
|
239
|
+
this.runtime.cancelContinuationWork();
|
|
240
|
+
this.runtime.clearGoalRecovery();
|
|
241
|
+
this.runtime.clearBudgetWrapUp();
|
|
242
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
243
|
+
this.runtime.pendingQueueAction = {
|
|
244
|
+
kind: "advance",
|
|
245
|
+
goalId: currentGoal.id,
|
|
246
|
+
reason: "skip",
|
|
247
|
+
completedText: currentGoal.text,
|
|
248
|
+
};
|
|
249
|
+
this.runtime.persistGoal(currentGoal);
|
|
250
|
+
notifyTerminal(ctx.ui, `Goal skip queued until Pi settles: ${currentGoal.text}`, "info");
|
|
251
|
+
if (ctx.isIdle?.() === true && !hasPendingMessages(ctx)) {
|
|
252
|
+
await this.dispatchPendingQueueActionIfSettled(ctx);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async resumeQueueAfterUnfreeze(ctx: StatusContext) {
|
|
257
|
+
if (this.runtime.queueFreezeAwaitingSettle) return false;
|
|
258
|
+
this.runtime.queueFrozen = false;
|
|
259
|
+
this.runtime.queueFreezeAwaitingSettle = false;
|
|
260
|
+
this.runtime.guardAbortGoalId = undefined;
|
|
261
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
262
|
+
if (this.runtime.activeGoal) {
|
|
263
|
+
if (this.runtime.activeGoal.status === "active" && this.runtime.activeGoal.activeStartedAt === undefined) {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
checkpointGoalActiveTime(this.runtime.activeGoal, now, true);
|
|
266
|
+
this.runtime.activeGoal.updatedAt = now;
|
|
267
|
+
}
|
|
268
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
269
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
270
|
+
} else {
|
|
271
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
272
|
+
}
|
|
273
|
+
if (this.runtime.pendingQueueAction) {
|
|
274
|
+
return this.dispatchPendingQueueActionIfSettled(ctx);
|
|
275
|
+
}
|
|
276
|
+
const goal = this.runtime.activeGoal;
|
|
277
|
+
if (goal?.status !== "active") return false;
|
|
278
|
+
this.runtime.requestContinuation(goal);
|
|
279
|
+
return this.runtime.dispatchContinuationIfSettled(ctx);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async dispatchPendingQueueActionIfSettled(ctx: StatusContext) {
|
|
283
|
+
const pending = this.runtime.pendingQueueAction;
|
|
284
|
+
if (!pending || this.runtime.queueFrozen) return false;
|
|
285
|
+
if (ctx.isIdle?.() !== true || hasPendingMessages(ctx)) return false;
|
|
286
|
+
if (pending.kind === "prioritize") {
|
|
287
|
+
this.runtime.pendingQueueAction = undefined;
|
|
288
|
+
return this.activatePrioritizedGoal(
|
|
289
|
+
pending.objective,
|
|
290
|
+
pending.tokenBudget,
|
|
291
|
+
ctx,
|
|
292
|
+
pending.displacedUsageFinalized === true,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
!this.runtime.activeGoal ||
|
|
297
|
+
this.runtime.activeGoal.id !== pending.goalId ||
|
|
298
|
+
(this.runtime.activeGoal.status !== "complete" && pending.reason === "complete")
|
|
299
|
+
) {
|
|
300
|
+
this.runtime.pendingQueueAction = undefined;
|
|
301
|
+
if (this.runtime.activeGoal) this.runtime.persistGoal(this.runtime.activeGoal);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const previousText = pending.completedText;
|
|
306
|
+
const reason = pending.reason;
|
|
307
|
+
this.runtime.pendingQueueAction = undefined;
|
|
308
|
+
this.runtime.cancelContinuationWork();
|
|
309
|
+
this.runtime.clearGoalRecovery();
|
|
310
|
+
this.runtime.clearBudgetWrapUp();
|
|
311
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
312
|
+
const next = skipQueuedGoal(this.runtime.queuedGoals);
|
|
313
|
+
this.runtime.queuedGoals = next.queue;
|
|
314
|
+
this.runtime.activeGoal = next.goal ? activateQueuedGoal(next.goal, currentTokenTotal(ctx)) : undefined;
|
|
315
|
+
if (!this.runtime.activeGoal) {
|
|
316
|
+
this.runtime.clearActiveGoal(ctx);
|
|
317
|
+
notifyTerminal(
|
|
318
|
+
ctx.ui,
|
|
319
|
+
reason === "complete"
|
|
320
|
+
? `Goal complete: ${previousText}. No goals remain.`
|
|
321
|
+
: `Goal skipped: ${previousText}. No goals remain.`,
|
|
322
|
+
"info",
|
|
323
|
+
);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
328
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
329
|
+
if (this.runtime.activeGoal.status !== "active") {
|
|
330
|
+
if (blocksStaleGoalToolCalls(this.runtime.activeGoal.status)) {
|
|
331
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
332
|
+
}
|
|
333
|
+
notifyTerminal(
|
|
334
|
+
ctx.ui,
|
|
335
|
+
`${reason === "complete" ? "Goal complete" : "Goal skipped"}: ${previousText}. Next goal remains ${this.runtime.activeGoal.status}: ${this.runtime.activeGoal.text}`,
|
|
336
|
+
"info",
|
|
337
|
+
);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
this.runtime.toolPolicy.prepareActivation(this.runtime.settings.toolVisibility, ctx);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.runtime.stopActiveGoal(ctx, {
|
|
345
|
+
kind: "activation_rollback",
|
|
346
|
+
expectedGoalId: this.runtime.activeGoal.id,
|
|
347
|
+
restoreGoal: this.runtime.activeGoal,
|
|
348
|
+
abortTurn: false,
|
|
349
|
+
});
|
|
350
|
+
notifyTerminal(ctx.ui, `Cannot start the next /goal: ${formatError(error)}`, "error");
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const activatedGoal = this.runtime.activeGoal;
|
|
354
|
+
const sent = await this.runtime.sendOwnedGoalPrompt(
|
|
355
|
+
ctx,
|
|
356
|
+
activatedGoal.id,
|
|
357
|
+
buildGoalPrompt(activatedGoal),
|
|
358
|
+
false, // Queue reactivation preserves its persisted safety epoch.
|
|
359
|
+
);
|
|
360
|
+
if (!sent && this.runtime.activeGoal?.id === activatedGoal.id) {
|
|
361
|
+
this.runtime.stopActiveGoal(ctx, {
|
|
362
|
+
kind: "activation_rollback",
|
|
363
|
+
expectedGoalId: activatedGoal.id,
|
|
364
|
+
restoreGoal: activatedGoal,
|
|
365
|
+
abortTurn: false,
|
|
366
|
+
});
|
|
367
|
+
notifyTerminal(ctx.ui, `Next goal paused after prompt delivery failed: ${activatedGoal.text}`, "warning");
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
notifyTerminal(
|
|
371
|
+
ctx.ui,
|
|
372
|
+
`${reason === "complete" ? "Goal complete" : "Goal skipped"}: ${previousText}. Started next goal: ${activatedGoal.text}`,
|
|
373
|
+
"info",
|
|
374
|
+
);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
notifyFrozenQueue(ctx: StatusContext) {
|
|
379
|
+
notifyTerminal(
|
|
380
|
+
ctx.ui,
|
|
381
|
+
"The experimental goal queue is frozen. Re-enable experimental.goals in pi-goal.json and run /reload, or use /goal clear.",
|
|
382
|
+
"warning",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
pauseGoal(ctx: StatusContext) {
|
|
387
|
+
if (!this.runtime.activeGoal) {
|
|
388
|
+
notifyTerminal(ctx.ui, "No active goal.", "info");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (this.runtime.activeGoal.status !== "active") {
|
|
392
|
+
notifyTerminal(ctx.ui, `Goal is ${this.runtime.activeGoal.status}; only active goals can be paused.`, "warning");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const stoppedGoal = this.runtime.stopActiveGoal(ctx, {
|
|
396
|
+
kind: "explicit_pause",
|
|
397
|
+
expectedGoalId: this.runtime.activeGoal.id,
|
|
398
|
+
});
|
|
399
|
+
if (stoppedGoal) notifyTerminal(ctx.ui, `Goal paused: ${stoppedGoal.text}`, "info");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async resumeGoal(ctx: StatusContext) {
|
|
403
|
+
if (!this.runtime.activeGoal) {
|
|
404
|
+
notifyTerminal(ctx.ui, "No active goal.", "info");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (!isResumableGoalStatus(this.runtime.activeGoal.status)) {
|
|
408
|
+
notifyTerminal(
|
|
409
|
+
ctx.ui,
|
|
410
|
+
`Goal is ${this.runtime.activeGoal.status}; only paused, blocked, usage-limited, or budget-limited goals can be resumed.`,
|
|
411
|
+
"warning",
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
this.runtime.activeGoal.tokenBudget !== undefined &&
|
|
417
|
+
this.runtime.activeGoal.tokensUsed >= this.runtime.activeGoal.tokenBudget
|
|
418
|
+
) {
|
|
419
|
+
notifyTerminal(ctx.ui, `Goal token budget is still reached: ${formatBudget(this.runtime.activeGoal)}`, "warning");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const goalToolVisibilityBeforeActivation = this.runtime.toolPolicy.snapshot();
|
|
423
|
+
try {
|
|
424
|
+
this.runtime.toolPolicy.prepareActivation(this.runtime.settings.toolVisibility, ctx);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
notifyTerminal(ctx.ui, `Cannot resume /goal: ${formatError(error)}`, "error");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const stoppedGoal = this.runtime.activeGoal;
|
|
430
|
+
const stoppedStatus = stoppedGoal.status;
|
|
431
|
+
this.runtime.cancelContinuationWork();
|
|
432
|
+
this.runtime.clearGoalRecovery();
|
|
433
|
+
this.runtime.clearBudgetWrapUp();
|
|
434
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
435
|
+
this.runtime.activeGoal = queueGoalSafetyReset(transitionGoal(nextGoalInstance(this.runtime.activeGoal), "active"));
|
|
436
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
437
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
438
|
+
if (this.runtime.activeGoal.status !== "active") {
|
|
439
|
+
notifyTerminal(ctx.ui, `Goal token budget is still reached: ${formatBudget(this.runtime.activeGoal)}`, "warning");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const resumedGoal = this.runtime.activeGoal;
|
|
443
|
+
const sent = await this.runtime.sendOwnedGoalPrompt(
|
|
444
|
+
ctx,
|
|
445
|
+
resumedGoal.id,
|
|
446
|
+
buildResumePrompt(resumedGoal, stoppedStatus),
|
|
447
|
+
);
|
|
448
|
+
if (!sent) {
|
|
449
|
+
if (this.runtime.activeGoal?.id === resumedGoal.id && this.runtime.activeGoal.status === "active") {
|
|
450
|
+
this.runtime.activeGoal = stoppedGoal;
|
|
451
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
452
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
453
|
+
if (blocksStaleGoalToolCalls(this.runtime.activeGoal.status)) {
|
|
454
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
455
|
+
}
|
|
456
|
+
this.runtime.toolPolicy.restore(goalToolVisibilityBeforeActivation);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const automaticLimit = this.runtime.settings.continuationLimits.automaticTurns;
|
|
461
|
+
notifyTerminal(
|
|
462
|
+
ctx.ui,
|
|
463
|
+
`Goal resumed from ${stoppedStatusLabel(stoppedStatus)}: ${resumedGoal.text}. ${
|
|
464
|
+
automaticLimit === null
|
|
465
|
+
? "Automatic work remains Unlimited; goal progress and cumulative usage are preserved."
|
|
466
|
+
: `The automatic-work counter will reset to 0 of ${automaticLimit} when the resumed prompt starts; goal progress and cumulative usage are preserved.`
|
|
467
|
+
}`,
|
|
468
|
+
automaticLimit === null ? "warning" : "info",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
clearGoal(ctx: StatusContext) {
|
|
473
|
+
if (!this.runtime.activeGoal) {
|
|
474
|
+
notifyTerminal(ctx.ui, "No active goal.", "info");
|
|
475
|
+
this.runtime.cancelContinuationWork();
|
|
476
|
+
this.runtime.clearGoalRecovery();
|
|
477
|
+
this.runtime.clearBudgetWrapUp();
|
|
478
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
479
|
+
this.runtime.clearPersistedGoal(ctx.cwd);
|
|
480
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const stoppedGoal = this.runtime.activeGoal.text;
|
|
485
|
+
this.runtime.clearActiveGoal(ctx);
|
|
486
|
+
notifyTerminal(ctx.ui, `Goal cleared: ${stoppedGoal}`, "warning");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async editGoal(objective: string, tokenBudget: number | undefined, ctx: StatusContext) {
|
|
490
|
+
const validationError = validateObjective(objective);
|
|
491
|
+
if (validationError) {
|
|
492
|
+
notifyTerminal(ctx.ui, validationError, "warning");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!this.runtime.activeGoal) {
|
|
496
|
+
notifyTerminal(ctx.ui, "No active goal. Use /goal <objective> to start one.", "warning");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.runtime.recordGoalUsage(this.runtime.activeGoal, ctx);
|
|
501
|
+
const previousGoal = { ...this.runtime.activeGoal };
|
|
502
|
+
this.runtime.cancelContinuationWork();
|
|
503
|
+
this.runtime.clearGoalRecovery();
|
|
504
|
+
this.runtime.clearBudgetWrapUp();
|
|
505
|
+
const previousStatus = this.runtime.activeGoal.status;
|
|
506
|
+
const rotatedGoal = nextGoalInstance(this.runtime.activeGoal);
|
|
507
|
+
const transitionedGoal = transitionGoal(
|
|
508
|
+
{
|
|
509
|
+
...rotatedGoal,
|
|
510
|
+
text: objective,
|
|
511
|
+
tokenBudget: tokenBudget ?? this.runtime.activeGoal.tokenBudget,
|
|
512
|
+
},
|
|
513
|
+
editedGoalStatus(previousStatus),
|
|
514
|
+
);
|
|
515
|
+
const nextGoal = transitionedGoal.status === "active" ? queueGoalSafetyReset(transitionedGoal) : transitionedGoal;
|
|
516
|
+
const goalToolVisibilityBeforeActivation =
|
|
517
|
+
nextGoal.status === "active" ? this.runtime.toolPolicy.snapshot() : undefined;
|
|
518
|
+
if (nextGoal.status === "active") {
|
|
519
|
+
try {
|
|
520
|
+
this.runtime.toolPolicy.prepareActivation(this.runtime.settings.toolVisibility, ctx);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
notifyTerminal(ctx.ui, `Cannot reactivate /goal: ${formatError(error)}`, "error");
|
|
523
|
+
if (this.runtime.activeGoal?.status === "active") {
|
|
524
|
+
this.runtime.pauseGoalForUnavailableTools(ctx);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
this.runtime.activeGoal = nextGoal;
|
|
530
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
531
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
532
|
+
const editedGoal = this.runtime.activeGoal;
|
|
533
|
+
if (!editedGoal) return;
|
|
534
|
+
if (editedGoal.status === "active") {
|
|
535
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
536
|
+
const sent = await this.runtime.sendOwnedGoalPrompt(ctx, editedGoal.id, buildObjectiveUpdatedPrompt(editedGoal));
|
|
537
|
+
if (!sent) {
|
|
538
|
+
if (this.runtime.activeGoal?.id === editedGoal.id) {
|
|
539
|
+
if (previousStatus === "active") {
|
|
540
|
+
this.runtime.stopActiveGoal(ctx, {
|
|
541
|
+
kind: "activation_rollback",
|
|
542
|
+
expectedGoalId: editedGoal.id,
|
|
543
|
+
restoreGoal: previousGoal,
|
|
544
|
+
abortTurn: true,
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
this.runtime.activeGoal = previousGoal;
|
|
548
|
+
if (blocksStaleGoalToolCalls(this.runtime.activeGoal.status)) {
|
|
549
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
550
|
+
} else {
|
|
551
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
552
|
+
}
|
|
553
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
554
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
555
|
+
}
|
|
556
|
+
if (goalToolVisibilityBeforeActivation) {
|
|
557
|
+
this.runtime.toolPolicy.restore(goalToolVisibilityBeforeActivation);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
} else if (blocksStaleGoalToolCalls(editedGoal.status)) {
|
|
563
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
564
|
+
} else {
|
|
565
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
566
|
+
}
|
|
567
|
+
notifyTerminal(ctx.ui, `Goal updated: ${objective}`, "info");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
showGoal(ctx: StatusContext) {
|
|
571
|
+
if (!this.runtime.activeGoal) {
|
|
572
|
+
const message = "Usage: /goal <objective>\nNo goal is currently set.";
|
|
573
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
574
|
+
this.reportGoalStatus(ctx, message);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (!this.runtime.queueFrozen) {
|
|
578
|
+
this.runtime.recordGoalUsage(this.runtime.activeGoal, ctx);
|
|
579
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
580
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
581
|
+
}
|
|
582
|
+
this.reportGoalStatus(
|
|
583
|
+
ctx,
|
|
584
|
+
goalSummary(
|
|
585
|
+
this.runtime.activeGoal,
|
|
586
|
+
this.runtime.queuedGoals,
|
|
587
|
+
this.runtime.settings.experimental.goals,
|
|
588
|
+
this.runtime.queueFrozen,
|
|
589
|
+
this.runtime.pendingQueueAction,
|
|
590
|
+
this.runtime.settings.continuationLimits.automaticTurns,
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private reportGoalStatus(ctx: StatusContext, message: string) {
|
|
596
|
+
if (ctx.mode === "print" || ctx.mode === "json") {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`/goal status is unavailable in ${ctx.mode} mode because Pi does not expose an extension-command output channel. Use TUI or RPC mode.`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
notifyTerminal(ctx.ui, message, "info");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private async activatePrioritizedGoal(
|
|
605
|
+
objective: string,
|
|
606
|
+
tokenBudget: number | undefined,
|
|
607
|
+
ctx: StatusContext,
|
|
608
|
+
displacedUsageFinalized = false,
|
|
609
|
+
) {
|
|
610
|
+
const currentGoal = this.runtime.activeGoal;
|
|
611
|
+
if (!currentGoal) {
|
|
612
|
+
await this.startGoal(objective, tokenBudget, ctx);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
if (currentGoal.status === "active" && !displacedUsageFinalized) {
|
|
616
|
+
this.runtime.recordGoalUsage(currentGoal, ctx);
|
|
617
|
+
}
|
|
618
|
+
const previousGoal = { ...currentGoal };
|
|
619
|
+
const previousQueue = [...this.runtime.queuedGoals];
|
|
620
|
+
const visibilityBeforeActivation = this.runtime.toolPolicy.snapshot();
|
|
621
|
+
try {
|
|
622
|
+
this.runtime.toolPolicy.prepareActivation(this.runtime.settings.toolVisibility, ctx);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
notifyTerminal(ctx.ui, `Cannot prioritize /goal: ${formatError(error)}`, "error");
|
|
625
|
+
if (currentGoal.status === "complete") {
|
|
626
|
+
// Completion already committed, so retain the priority intent for a
|
|
627
|
+
// later /reload after the tool policy is restored.
|
|
628
|
+
this.runtime.pendingQueueAction = {
|
|
629
|
+
kind: "prioritize",
|
|
630
|
+
objective,
|
|
631
|
+
tokenBudget,
|
|
632
|
+
...(displacedUsageFinalized ? { displacedUsageFinalized: true } : {}),
|
|
633
|
+
};
|
|
634
|
+
this.runtime.persistGoal(currentGoal);
|
|
635
|
+
} else {
|
|
636
|
+
// Roll back an activation that never started. An active displaced goal
|
|
637
|
+
// cannot continue safely without its terminal tools, so make it resumable.
|
|
638
|
+
this.runtime.pendingQueueAction = undefined;
|
|
639
|
+
if (currentGoal.status === "active") {
|
|
640
|
+
this.runtime.pauseGoalForUnavailableTools(ctx, true, !displacedUsageFinalized);
|
|
641
|
+
} else {
|
|
642
|
+
this.runtime.persistGoal(currentGoal);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.runtime.cancelContinuationWork();
|
|
649
|
+
this.runtime.clearGoalRecovery();
|
|
650
|
+
this.runtime.clearBudgetWrapUp();
|
|
651
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
652
|
+
const prioritized = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
|
|
653
|
+
const next =
|
|
654
|
+
currentGoal.status === "complete"
|
|
655
|
+
? { goal: prioritized, queue: [...this.runtime.queuedGoals] }
|
|
656
|
+
: prioritizeQueuedGoal(currentGoal, this.runtime.queuedGoals, prioritized);
|
|
657
|
+
this.runtime.activeGoal = next.goal;
|
|
658
|
+
this.runtime.queuedGoals = next.queue;
|
|
659
|
+
this.runtime.pendingQueueAction = undefined;
|
|
660
|
+
if (!this.runtime.activeGoal) return false;
|
|
661
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
662
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
663
|
+
const sent = await this.runtime.sendOwnedGoalPrompt(
|
|
664
|
+
ctx,
|
|
665
|
+
this.runtime.activeGoal.id,
|
|
666
|
+
buildGoalPrompt(this.runtime.activeGoal),
|
|
667
|
+
);
|
|
668
|
+
if (!sent && this.runtime.activeGoal.id === prioritized.id) {
|
|
669
|
+
this.runtime.queuedGoals = previousQueue;
|
|
670
|
+
if (previousGoal.status === "active") {
|
|
671
|
+
this.runtime.stopActiveGoal(ctx, {
|
|
672
|
+
kind: "activation_rollback",
|
|
673
|
+
expectedGoalId: prioritized.id,
|
|
674
|
+
restoreGoal: previousGoal,
|
|
675
|
+
abortTurn: true,
|
|
676
|
+
});
|
|
677
|
+
} else {
|
|
678
|
+
this.runtime.activeGoal = previousGoal;
|
|
679
|
+
if (previousGoal.status === "complete") {
|
|
680
|
+
this.runtime.pendingQueueAction = { kind: "prioritize", objective, tokenBudget };
|
|
681
|
+
} else if (blocksStaleGoalToolCalls(previousGoal.status)) {
|
|
682
|
+
this.runtime.blockStaleGoalToolCalls();
|
|
683
|
+
}
|
|
684
|
+
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
685
|
+
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
686
|
+
}
|
|
687
|
+
this.runtime.toolPolicy.restore(visibilityBeforeActivation);
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
notifyTerminal(ctx.ui, `Goal prioritized: ${objective}`, "info");
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
}
|