@signalridge/pi-goal 1.4.2 → 1.4.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/CHANGELOG.md +15 -0
- package/README.md +11 -6
- package/package.json +8 -8
- package/src/accounting.ts +37 -3
- package/src/commands.ts +123 -17
- package/src/goal.ts +1 -1
- package/src/lifecycle.ts +327 -25
- package/src/menu.ts +1 -1
- package/src/persistence.ts +12 -0
- package/src/prompts.ts +8 -1
- package/src/queue.ts +2 -3
- package/src/run-protocol.ts +15 -0
- package/src/runtime.ts +258 -24
- package/src/tools.ts +16 -4
- package/src/wait.ts +6 -2
package/src/lifecycle.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { currentTokenTotal } from "./accounting.js";
|
|
2
|
+
import { assistantTokensAfterGoalState, currentTokenTotal, rebaseGoalUsage } from "./accounting.js";
|
|
3
3
|
import type { GoalCommandController } from "./commands.js";
|
|
4
4
|
import { notifyTerminal } from "./errors.js";
|
|
5
5
|
import { type ActiveGoal, loadGoalStateFromSession } from "./persistence.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildGoalBindingUpdate,
|
|
8
|
+
buildGoalPrompt,
|
|
9
|
+
buildGoalSystemPrompt,
|
|
10
|
+
GOAL_BINDING_UPDATE_HEADER,
|
|
11
|
+
} from "./prompts.js";
|
|
7
12
|
import { activateQueuedGoal } from "./queue.js";
|
|
8
13
|
import type { GoalRunController } from "./run-protocol.js";
|
|
9
14
|
import {
|
|
@@ -13,6 +18,7 @@ import {
|
|
|
13
18
|
findFinalAssistantMessage,
|
|
14
19
|
formatError,
|
|
15
20
|
type GoalRuntime,
|
|
21
|
+
hasPendingMessages,
|
|
16
22
|
incrementGoal,
|
|
17
23
|
isGoalContextOverflow,
|
|
18
24
|
isRetryableGoalInterruption,
|
|
@@ -39,13 +45,50 @@ export function registerGoalLifecycle(
|
|
|
39
45
|
runController: GoalRunController,
|
|
40
46
|
options: GoalLifecycleOptions = {},
|
|
41
47
|
) {
|
|
48
|
+
function afterTreeNavigationSettles(ctx: StatusContext, ownsWork: () => boolean, work: () => Promise<unknown>) {
|
|
49
|
+
const generation = runtime.menuGeneration;
|
|
50
|
+
const signal = runtime.menuController.signal;
|
|
51
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
52
|
+
const cancel = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
signal.removeEventListener("abort", cancel);
|
|
55
|
+
};
|
|
56
|
+
const attempt = () => {
|
|
57
|
+
if (signal.aborted || generation !== runtime.menuGeneration || !ownsWork()) {
|
|
58
|
+
cancel();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (ctx.isIdle?.() !== true || hasPendingMessages(ctx)) {
|
|
62
|
+
timer = setTimeout(attempt, 1_000);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
cancel();
|
|
66
|
+
void work().catch((error) => {
|
|
67
|
+
if (!signal.aborted && generation === runtime.menuGeneration) {
|
|
68
|
+
notifyTerminal(ctx.ui, `Cannot restore /goal after tree navigation: ${formatError(error)}`, "error");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
73
|
+
timer = setTimeout(attempt, 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
pi.on("session_start", async (_event, ctx) => {
|
|
43
77
|
runtime.replaceMenuSession();
|
|
78
|
+
runtime.inputWakeGoalId = undefined;
|
|
79
|
+
runtime.inputWakeNeedsPrompt = false;
|
|
80
|
+
runtime.pendingDirectInputs = [];
|
|
81
|
+
runtime.ambiguousWaitInputGoalId = undefined;
|
|
82
|
+
runtime.acceptedRunPrompt = undefined;
|
|
83
|
+
runtime.pendingInputWake = undefined;
|
|
84
|
+
runtime.clearGoalWaitWake();
|
|
44
85
|
runtime.clearCompletionStatusTimer();
|
|
45
86
|
runtime.clearContinuationTracking();
|
|
46
87
|
runtime.clearPendingGoalPrompts();
|
|
47
88
|
runtime.clearAgentRun();
|
|
48
89
|
runtime.guardAbortGoalId = undefined;
|
|
90
|
+
runtime.pendingStoppedUsageGoalId = undefined;
|
|
91
|
+
runtime.pendingReplacementUsage = undefined;
|
|
49
92
|
runtime.clearGoalRecovery();
|
|
50
93
|
runtime.clearBudgetWrapUp();
|
|
51
94
|
runtime.clearStaleGoalToolCallBlock();
|
|
@@ -100,6 +143,12 @@ export function registerGoalLifecycle(
|
|
|
100
143
|
runtime.activeGoal = resetGoalSafetyEpoch(runtime.activeGoal);
|
|
101
144
|
}
|
|
102
145
|
if (runtime.activeGoal.status === "active") {
|
|
146
|
+
if (runtime.activeGoal.usageBaselinePending) {
|
|
147
|
+
runtime.activeGoal = rebaseGoalUsage(
|
|
148
|
+
{ ...runtime.activeGoal, usageBaselinePending: undefined },
|
|
149
|
+
currentTokenTotal(ctx),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
103
152
|
runtime.recordGoalUsage(runtime.activeGoal, ctx);
|
|
104
153
|
if (runtime.limitActiveGoalForBudget(ctx, false)) return;
|
|
105
154
|
if (runtime.enforceAutomaticTurnLimit(ctx, false) || runtime.enforceNoProgressLimit(ctx)) return;
|
|
@@ -113,6 +162,7 @@ export function registerGoalLifecycle(
|
|
|
113
162
|
}
|
|
114
163
|
runtime.persistGoal(runtime.activeGoal);
|
|
115
164
|
runtime.updateStatus(ctx, runtime.activeGoal);
|
|
165
|
+
runtime.scheduleGoalWaitWake(ctx);
|
|
116
166
|
if (startRestoredQueuedGoal) {
|
|
117
167
|
const restoredGoal = runtime.activeGoal;
|
|
118
168
|
const sent = await runtime.sendOwnedGoalPrompt(
|
|
@@ -136,9 +186,123 @@ export function registerGoalLifecycle(
|
|
|
136
186
|
}
|
|
137
187
|
});
|
|
138
188
|
|
|
189
|
+
pi.on("session_before_tree", (_event, ctx) => {
|
|
190
|
+
const goal = runtime.activeGoal;
|
|
191
|
+
if (runtime.queueFrozen || goal?.status !== "active") return;
|
|
192
|
+
if (!runtime.recordGoalUsage(goal, ctx)) return;
|
|
193
|
+
runtime.persistGoal(goal);
|
|
194
|
+
runtime.updateStatus(ctx, goal);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
198
|
+
runtime.replaceMenuSession();
|
|
199
|
+
runtime.clearCompletionStatusTimer();
|
|
200
|
+
runtime.cancelContinuationWork();
|
|
201
|
+
runtime.clearContinuationTracking();
|
|
202
|
+
runtime.clearPendingGoalPrompts();
|
|
203
|
+
runtime.clearAgentRun();
|
|
204
|
+
runtime.pendingNonGoalInputs = [];
|
|
205
|
+
runtime.pendingDirectInputs = [];
|
|
206
|
+
runtime.ambiguousWaitInputGoalId = undefined;
|
|
207
|
+
runtime.acceptedRunPrompt = undefined;
|
|
208
|
+
runtime.inputWakeGoalId = undefined;
|
|
209
|
+
runtime.inputWakeNeedsPrompt = false;
|
|
210
|
+
runtime.guardAbortGoalId = undefined;
|
|
211
|
+
runtime.pendingStoppedUsageGoalId = undefined;
|
|
212
|
+
runtime.pendingReplacementUsage = undefined;
|
|
213
|
+
runtime.clearGoalRecovery();
|
|
214
|
+
runtime.clearBudgetWrapUp();
|
|
215
|
+
runtime.clearStaleGoalToolCallBlock();
|
|
216
|
+
runtime.clearTerminalDetails();
|
|
217
|
+
|
|
218
|
+
const loaded = loadGoalStateFromSession(ctx);
|
|
219
|
+
runtime.activeGoal = loaded.goal;
|
|
220
|
+
runtime.queuedGoals = loaded.queue;
|
|
221
|
+
runtime.pendingQueueAction = loaded.pendingAction;
|
|
222
|
+
runtime.queueFrozen = loaded.hasExperimentalQueueState && !runtime.settings.experimental.goals;
|
|
223
|
+
if (runtime.queueFrozen) {
|
|
224
|
+
runController.handleTreeNavigation(undefined);
|
|
225
|
+
ctx.ui.setStatus(STATUS_KEY, "queue off");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!runtime.activeGoal) {
|
|
229
|
+
runController.handleTreeNavigation(undefined);
|
|
230
|
+
runtime.toolPolicy.reconcileRestoredState(runtime.settings.toolVisibility, false);
|
|
231
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let startQueuedHead = false;
|
|
235
|
+
if (runtime.activeGoal.status === "queued" && !runtime.pendingQueueAction) {
|
|
236
|
+
runtime.activeGoal = activateQueuedGoal(runtime.activeGoal, currentTokenTotal(ctx));
|
|
237
|
+
startQueuedHead = runtime.activeGoal.status === "active";
|
|
238
|
+
}
|
|
239
|
+
runController.handleTreeNavigation(runtime.activeGoal.id);
|
|
240
|
+
runtime.toolPolicy.reconcileRestoredState(runtime.settings.toolVisibility, true);
|
|
241
|
+
const pending = runtime.pendingQueueAction;
|
|
242
|
+
if (pending) {
|
|
243
|
+
afterTreeNavigationSettles(
|
|
244
|
+
ctx,
|
|
245
|
+
() => runtime.pendingQueueAction === pending,
|
|
246
|
+
() => commands.dispatchPendingQueueActionIfSettled(ctx),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (runtime.activeGoal.status === "active") {
|
|
250
|
+
const usageIsFinalized =
|
|
251
|
+
runtime.pendingQueueAction?.kind === "prioritize" &&
|
|
252
|
+
runtime.pendingQueueAction.displacedUsageFinalized === true;
|
|
253
|
+
const unaccountedTokens =
|
|
254
|
+
usageIsFinalized || startQueuedHead || runtime.activeGoal.usageBaselinePending
|
|
255
|
+
? 0
|
|
256
|
+
: assistantTokensAfterGoalState(ctx, loaded.source === "legacy-goals" ? "goals-state" : "goal-state");
|
|
257
|
+
const goalWithUsage = {
|
|
258
|
+
...runtime.activeGoal,
|
|
259
|
+
tokensUsed: Math.min(Number.MAX_SAFE_INTEGER, runtime.activeGoal.tokensUsed + unaccountedTokens),
|
|
260
|
+
};
|
|
261
|
+
runtime.activeGoal = rebaseGoalUsage(
|
|
262
|
+
{ ...goalWithUsage, usageBaselinePending: undefined },
|
|
263
|
+
currentTokenTotal(ctx),
|
|
264
|
+
);
|
|
265
|
+
if (runtime.activeGoal.safetyResetPending) runtime.activeGoal = resetGoalSafetyEpoch(runtime.activeGoal);
|
|
266
|
+
if (runtime.limitActiveGoalForBudget(ctx, false)) return;
|
|
267
|
+
if (runtime.enforceAutomaticTurnLimit(ctx, false) || runtime.enforceNoProgressLimit(ctx)) return;
|
|
268
|
+
if (!runtime.toolPolicy.toolsAvailable()) {
|
|
269
|
+
runtime.pauseGoalForUnavailableTools(ctx, false, false);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
runtime.persistGoal(runtime.activeGoal);
|
|
274
|
+
runtime.updateStatus(ctx, runtime.activeGoal);
|
|
275
|
+
runtime.scheduleGoalWaitWake(ctx);
|
|
276
|
+
if (startQueuedHead) {
|
|
277
|
+
const queuedHead = runtime.activeGoal;
|
|
278
|
+
afterTreeNavigationSettles(
|
|
279
|
+
ctx,
|
|
280
|
+
() => runtime.activeGoal?.id === queuedHead.id && runtime.activeGoal.status === "active",
|
|
281
|
+
async () => {
|
|
282
|
+
const sent = await runtime.sendOwnedGoalPrompt(ctx, queuedHead.id, buildGoalPrompt(queuedHead), false);
|
|
283
|
+
if (!sent && runtime.activeGoal?.id === queuedHead.id) {
|
|
284
|
+
runtime.stopActiveGoal(ctx, {
|
|
285
|
+
kind: "activation_rollback",
|
|
286
|
+
expectedGoalId: queuedHead.id,
|
|
287
|
+
restoreGoal: queuedHead,
|
|
288
|
+
abortTurn: false,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
139
296
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
140
297
|
runController.unbindSession();
|
|
141
298
|
runtime.closeMenuSession();
|
|
299
|
+
runtime.inputWakeGoalId = undefined;
|
|
300
|
+
runtime.inputWakeNeedsPrompt = false;
|
|
301
|
+
runtime.pendingDirectInputs = [];
|
|
302
|
+
runtime.ambiguousWaitInputGoalId = undefined;
|
|
303
|
+
runtime.acceptedRunPrompt = undefined;
|
|
304
|
+
runtime.pendingInputWake = undefined;
|
|
305
|
+
runtime.clearGoalWaitWake();
|
|
142
306
|
if (runtime.activeGoal) {
|
|
143
307
|
if (!runtime.queueFrozen && runtime.activeGoal.status === "active") {
|
|
144
308
|
runtime.recordGoalUsage(runtime.activeGoal, ctx, false);
|
|
@@ -149,6 +313,8 @@ export function registerGoalLifecycle(
|
|
|
149
313
|
runtime.clearPendingGoalPrompts();
|
|
150
314
|
runtime.clearAgentRun();
|
|
151
315
|
runtime.guardAbortGoalId = undefined;
|
|
316
|
+
runtime.pendingStoppedUsageGoalId = undefined;
|
|
317
|
+
runtime.pendingReplacementUsage = undefined;
|
|
152
318
|
runtime.clearGoalRecovery();
|
|
153
319
|
runtime.clearBudgetWrapUp();
|
|
154
320
|
runtime.clearStaleGoalToolCallBlock();
|
|
@@ -182,6 +348,7 @@ export function registerGoalLifecycle(
|
|
|
182
348
|
if (runtime.activeGoal?.status !== "active") {
|
|
183
349
|
runtime.clearGoalRecovery();
|
|
184
350
|
if (runtime.pendingQueueAction) await commands.dispatchPendingQueueActionIfSettled(ctx);
|
|
351
|
+
runtime.scheduleGoalWaitWake(ctx);
|
|
185
352
|
return;
|
|
186
353
|
}
|
|
187
354
|
|
|
@@ -217,7 +384,11 @@ export function registerGoalLifecycle(
|
|
|
217
384
|
runtime.scheduleContinuationDispatch(ctx, runtime.activeGoal.id);
|
|
218
385
|
});
|
|
219
386
|
|
|
220
|
-
pi.on("
|
|
387
|
+
pi.on("session_compact_failed", (_event, ctx) => {
|
|
388
|
+
runtime.scheduleGoalWaitWake(ctx);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
pi.on("input", (event, _ctx) => {
|
|
221
392
|
if (event.source === "extension") {
|
|
222
393
|
if (runtime.consumeCancelledContinuationPrompt(event.text) || runtime.consumeStaleOwnedGoalPrompt(event.text)) {
|
|
223
394
|
return { action: "handled" as const };
|
|
@@ -229,8 +400,9 @@ export function registerGoalLifecycle(
|
|
|
229
400
|
if (runtime.hasPendingOwnedGoalPrompt(event.text)) return;
|
|
230
401
|
if (event.streamingBehavior === "steer" || event.streamingBehavior === "followUp") {
|
|
231
402
|
runtime.noteQueuedNonGoalInput(event.text, event.streamingBehavior);
|
|
403
|
+
} else {
|
|
404
|
+
runtime.noteDirectInput(event.text, false);
|
|
232
405
|
}
|
|
233
|
-
runtime.clearGoalRecovery();
|
|
234
406
|
return;
|
|
235
407
|
}
|
|
236
408
|
if (runtime.queueFrozen) return;
|
|
@@ -240,12 +412,10 @@ export function registerGoalLifecycle(
|
|
|
240
412
|
return;
|
|
241
413
|
}
|
|
242
414
|
if (event.streamingBehavior === "steer") {
|
|
243
|
-
runtime.noteQueuedNonGoalInput(event.text, "steer");
|
|
415
|
+
runtime.noteQueuedNonGoalInput(event.text, "steer", true);
|
|
416
|
+
} else {
|
|
417
|
+
runtime.noteDirectInput(event.text, true);
|
|
244
418
|
}
|
|
245
|
-
runtime.clearGoalRecovery();
|
|
246
|
-
runtime.clearBudgetWrapUp();
|
|
247
|
-
runtime.clearStaleGoalToolCallBlock();
|
|
248
|
-
runtime.resetActiveSafetyEpoch(ctx);
|
|
249
419
|
});
|
|
250
420
|
|
|
251
421
|
pi.on("message_start", (event, ctx) => {
|
|
@@ -259,7 +429,11 @@ export function registerGoalLifecycle(
|
|
|
259
429
|
return;
|
|
260
430
|
}
|
|
261
431
|
if (message.role === "custom") {
|
|
262
|
-
if (runtime.isActiveBudgetWrapUpMessage(message))
|
|
432
|
+
if (runtime.isActiveBudgetWrapUpMessage(message)) {
|
|
433
|
+
runtime.beginAgentRun(runtime.activeGoal?.id, "manual");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
runtime.finalizeStoppedRunUsage(ctx);
|
|
263
437
|
if (runtime.guardAbortGoalId === runtime.activeGoal?.id) {
|
|
264
438
|
runtime.guardAbortGoalId = undefined;
|
|
265
439
|
}
|
|
@@ -278,18 +452,77 @@ export function registerGoalLifecycle(
|
|
|
278
452
|
: "";
|
|
279
453
|
const ownedPrompt = runtime.consumeOwnedGoalPrompt(prompt);
|
|
280
454
|
const ownedPromptBoundary = runtime.hasOwnedPromptBoundary(prompt);
|
|
281
|
-
const
|
|
455
|
+
const acceptedRunPrompt = runtime.acceptedRunPrompt;
|
|
456
|
+
const initialRunMessage =
|
|
457
|
+
acceptedRunPrompt !== undefined &&
|
|
458
|
+
(prompt === acceptedRunPrompt || prompt.startsWith(`${acceptedRunPrompt}\n\n`));
|
|
459
|
+
runtime.acceptedRunPrompt = undefined;
|
|
460
|
+
if (initialRunMessage && runtime.pendingInputWake) {
|
|
461
|
+
const ticket = runtime.pendingInputWake;
|
|
462
|
+
runtime.pendingInputWake = undefined;
|
|
463
|
+
if (!commands.commitWaitingGoalOnInput(ctx, ticket)) {
|
|
464
|
+
runtime.beginAgentRun(null, undefined);
|
|
465
|
+
abortCurrentTurn(ctx);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const classifyInput = !ownedPromptBoundary && !initialRunMessage;
|
|
470
|
+
const collidingCandidates =
|
|
471
|
+
classifyInput && runtime.hasDirectInputCandidate(prompt) && runtime.hasQueuedInputCandidate(prompt);
|
|
472
|
+
const exactDirectInput = classifyInput ? runtime.consumeDirectInput(prompt, false) : undefined;
|
|
473
|
+
const exactQueuedInput = classifyInput ? runtime.consumeQueuedNonGoalInput(prompt, false) : undefined;
|
|
474
|
+
const agreeingRealInputs = Boolean(exactDirectInput?.realInput && exactQueuedInput?.realInput);
|
|
475
|
+
if (agreeingRealInputs && exactQueuedInput) runtime.pendingNonGoalInputs.unshift(exactQueuedInput);
|
|
476
|
+
const ambiguousInput =
|
|
477
|
+
!agreeingRealInputs && (collidingCandidates || Boolean(exactDirectInput && exactQueuedInput));
|
|
478
|
+
const deliveredDirectInput = ambiguousInput
|
|
479
|
+
? exactDirectInput
|
|
480
|
+
? { ...exactDirectInput, realInput: false }
|
|
481
|
+
: undefined
|
|
482
|
+
: (exactDirectInput ??
|
|
483
|
+
(exactQueuedInput ? undefined : classifyInput ? runtime.consumeDirectInput(prompt) : undefined));
|
|
484
|
+
const queuedNonGoalInput = ambiguousInput
|
|
485
|
+
? exactQueuedInput
|
|
486
|
+
? { ...exactQueuedInput, realInput: false }
|
|
487
|
+
: undefined
|
|
488
|
+
: !classifyInput || deliveredDirectInput
|
|
489
|
+
? undefined
|
|
490
|
+
: (exactQueuedInput ?? runtime.consumeQueuedNonGoalInput(prompt));
|
|
491
|
+
if (!ownedPrompt && ownedPromptBoundary) return;
|
|
282
492
|
if (!ownedPrompt) {
|
|
283
|
-
if (
|
|
284
|
-
|
|
493
|
+
if (initialRunMessage || deliveredDirectInput || queuedNonGoalInput) {
|
|
494
|
+
runtime.clearGoalRecovery();
|
|
495
|
+
if (deliveredDirectInput?.realInput || queuedNonGoalInput?.realInput) runtime.clearBudgetWrapUp();
|
|
496
|
+
runtime.clearStaleGoalToolCallBlock();
|
|
497
|
+
if (runtime.guardAbortGoalId === runtime.activeGoal?.id) runtime.guardAbortGoalId = undefined;
|
|
498
|
+
}
|
|
499
|
+
if (deliveredDirectInput?.realInput && deliveredDirectInput.waiting) {
|
|
500
|
+
if (commands.resumeWaitingGoalOnInput(ctx, true)) runtime.guardAbortGoalId = undefined;
|
|
501
|
+
} else if (deliveredDirectInput?.realInput) {
|
|
502
|
+
runtime.resetActiveSafetyEpoch(ctx);
|
|
503
|
+
} else if (queuedNonGoalInput?.behavior === "followUp") {
|
|
504
|
+
beginNonGoalFollowUp(ctx, queuedNonGoalInput.realInput, true);
|
|
505
|
+
} else if (queuedNonGoalInput?.realInput) {
|
|
506
|
+
if (commands.resumeWaitingGoalOnInput(ctx, true)) {
|
|
507
|
+
runtime.guardAbortGoalId = undefined;
|
|
508
|
+
} else if (queuedNonGoalInput.realInput) {
|
|
509
|
+
runtime.resetActiveSafetyEpoch(ctx);
|
|
510
|
+
}
|
|
285
511
|
}
|
|
512
|
+
if (runtime.activeGoal?.status !== "active") runtime.beginAgentRun(null, undefined);
|
|
286
513
|
return;
|
|
287
514
|
}
|
|
288
515
|
if (runtime.activeGoal?.id !== ownedPrompt.goalId || runtime.activeGoal.status !== "active") {
|
|
289
516
|
return;
|
|
290
517
|
}
|
|
291
|
-
if (
|
|
292
|
-
runtime.activeGoal.
|
|
518
|
+
if (
|
|
519
|
+
runtime.activeGoal.usageBaselinePending ||
|
|
520
|
+
(runtime.agentRunGoalId !== undefined && runtime.agentRunGoalId !== ownedPrompt.goalId)
|
|
521
|
+
) {
|
|
522
|
+
runtime.activeGoal = rebaseGoalUsage(
|
|
523
|
+
{ ...runtime.activeGoal, usageBaselinePending: undefined },
|
|
524
|
+
currentTokenTotal(ctx),
|
|
525
|
+
);
|
|
293
526
|
}
|
|
294
527
|
runtime.beginAgentRun(ownedPrompt.goalId, "manual");
|
|
295
528
|
if (ownedPrompt.resetSafetyEpoch) {
|
|
@@ -306,6 +539,22 @@ export function registerGoalLifecycle(
|
|
|
306
539
|
// context transformation aborts before the provider adapter receives the signal.
|
|
307
540
|
abortCurrentTurn(ctx);
|
|
308
541
|
}
|
|
542
|
+
const goal = runtime.activeGoal;
|
|
543
|
+
if (!runtime.queueFrozen && goal?.status === "active" && runtime.inputWakeGoalId === goal.id) {
|
|
544
|
+
// Native queued input reaches message_start without before_agent_start.
|
|
545
|
+
// Keep the rotated guard visible for every remaining response in this run,
|
|
546
|
+
// without enqueueing another user turn or changing persisted user input.
|
|
547
|
+
messages.push({
|
|
548
|
+
role: "custom",
|
|
549
|
+
customType: "goal-input-wake",
|
|
550
|
+
content: runtime.inputWakeNeedsPrompt
|
|
551
|
+
? `${GOAL_BINDING_UPDATE_HEADER}\n\n${buildGoalSystemPrompt(goal)}`
|
|
552
|
+
: buildGoalBindingUpdate(goal),
|
|
553
|
+
display: false,
|
|
554
|
+
timestamp: Date.now(),
|
|
555
|
+
});
|
|
556
|
+
return { messages };
|
|
557
|
+
}
|
|
309
558
|
if (messages.length !== event.messages.length) return { messages };
|
|
310
559
|
});
|
|
311
560
|
|
|
@@ -372,6 +621,10 @@ export function registerGoalLifecycle(
|
|
|
372
621
|
});
|
|
373
622
|
|
|
374
623
|
pi.on("before_agent_start", (event, ctx) => {
|
|
624
|
+
runtime.pendingInputWake = undefined;
|
|
625
|
+
runtime.acceptedRunPrompt = typeof event.prompt === "string" ? event.prompt : undefined;
|
|
626
|
+
runtime.inputWakeGoalId = undefined;
|
|
627
|
+
runtime.inputWakeNeedsPrompt = false;
|
|
375
628
|
runtime.clearAgentRun();
|
|
376
629
|
if (runtime.queueFrozen) return;
|
|
377
630
|
// Pi-owned retries emit agent_start directly. Reaching a normal prompt
|
|
@@ -384,15 +637,36 @@ export function registerGoalLifecycle(
|
|
|
384
637
|
const ownedPromptGoalId = goalPromptGoalId ?? continuationGoalId;
|
|
385
638
|
const ownedPromptBoundary = runtime.hasOwnedPromptBoundary(event.prompt);
|
|
386
639
|
const activeBudgetWrapUp = runtime.hasActiveBudgetWrapUp();
|
|
640
|
+
const recoveryBeforeInput = runtime.hasActiveGoalRecovery();
|
|
641
|
+
const acceptedDirectInput =
|
|
642
|
+
ownedPromptBoundary || ownedPromptGoalId !== undefined ? undefined : runtime.consumeDirectInput(event.prompt);
|
|
643
|
+
const queuedNonGoalInput =
|
|
644
|
+
activeBudgetWrapUp || acceptedDirectInput
|
|
645
|
+
? undefined
|
|
646
|
+
: runtime.consumeQueuedNonGoalInput(
|
|
647
|
+
event.prompt,
|
|
648
|
+
!recoveryBeforeInput && ownedPromptGoalId === undefined && !ownedPromptBoundary,
|
|
649
|
+
);
|
|
650
|
+
if (acceptedDirectInput || queuedNonGoalInput) runtime.clearGoalRecovery();
|
|
387
651
|
const activeGoalRecovery = runtime.hasActiveGoalRecovery();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
652
|
+
if (acceptedDirectInput?.realInput || queuedNonGoalInput?.realInput) {
|
|
653
|
+
const ticket = commands.prepareWaitingGoalOnInput(ctx);
|
|
654
|
+
if (ticket) {
|
|
655
|
+
runtime.pendingInputWake = ticket;
|
|
656
|
+
runtime.beginAgentRun(null, undefined);
|
|
657
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${buildGoalSystemPrompt(ticket.resumedGoal)}` };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (!ownedPromptBoundary && ownedPromptGoalId === undefined && acceptedDirectInput?.realInput) {
|
|
661
|
+
if (acceptedDirectInput.waiting) commands.resumeWaitingGoalOnInput(ctx);
|
|
662
|
+
else runtime.resetActiveSafetyEpoch(ctx);
|
|
663
|
+
}
|
|
394
664
|
if (queuedNonGoalInput?.behavior === "followUp") {
|
|
395
|
-
beginNonGoalFollowUp(ctx, queuedNonGoalInput.
|
|
665
|
+
beginNonGoalFollowUp(ctx, queuedNonGoalInput.realInput, false);
|
|
666
|
+
} else if (queuedNonGoalInput?.realInput) {
|
|
667
|
+
if (!commands.resumeWaitingGoalOnInput(ctx) && queuedNonGoalInput.realInput) {
|
|
668
|
+
runtime.resetActiveSafetyEpoch(ctx);
|
|
669
|
+
}
|
|
396
670
|
}
|
|
397
671
|
const runOrigin = continuationGoalId
|
|
398
672
|
? "automatic"
|
|
@@ -443,6 +717,12 @@ export function registerGoalLifecycle(
|
|
|
443
717
|
runtime.pauseGoalForUnavailableTools(ctx, ownedPromptGoalId !== undefined);
|
|
444
718
|
return;
|
|
445
719
|
}
|
|
720
|
+
if (goalPromptGoalId === runtime.activeGoal.id && runtime.activeGoal.usageBaselinePending) {
|
|
721
|
+
runtime.activeGoal = rebaseGoalUsage(
|
|
722
|
+
{ ...runtime.activeGoal, usageBaselinePending: undefined },
|
|
723
|
+
currentTokenTotal(ctx),
|
|
724
|
+
);
|
|
725
|
+
}
|
|
446
726
|
if (goalPrompt?.resetSafetyEpoch && goalPromptGoalId === runtime.activeGoal.id) {
|
|
447
727
|
runtime.activeGoal = resetGoalSafetyEpoch(runtime.activeGoal);
|
|
448
728
|
runtime.persistGoal(runtime.activeGoal);
|
|
@@ -477,6 +757,21 @@ export function registerGoalLifecycle(
|
|
|
477
757
|
|
|
478
758
|
pi.on("agent_end", (event, ctx) => {
|
|
479
759
|
const run = runtime.finishAgentRun();
|
|
760
|
+
const replacement = runtime.pendingReplacementUsage;
|
|
761
|
+
if (replacement && replacement.runGoalId === run.goalId) {
|
|
762
|
+
runtime.pendingReplacementUsage = undefined;
|
|
763
|
+
const successor = runtime.activeGoal;
|
|
764
|
+
if (successor?.id === replacement.replacementGoalId) {
|
|
765
|
+
const rebasedSuccessor = rebaseGoalUsage(
|
|
766
|
+
{ ...successor, usageBaselinePending: undefined },
|
|
767
|
+
currentTokenTotal(ctx),
|
|
768
|
+
);
|
|
769
|
+
runtime.activeGoal = rebasedSuccessor;
|
|
770
|
+
runtime.persistGoal(rebasedSuccessor);
|
|
771
|
+
if (runtime.activeGoal === rebasedSuccessor) runtime.updateStatus(ctx, rebasedSuccessor);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (runtime.finalizeStoppedRunUsage(ctx, run.goalId)) return;
|
|
480
775
|
if (runtime.queueFrozen || run.goalId === null) return;
|
|
481
776
|
if (!runtime.canRecordGoalUsage() && !runtime.hasActiveBudgetWrapUp()) return;
|
|
482
777
|
if (run.goalId && run.goalId !== runtime.activeGoal?.id) return;
|
|
@@ -582,15 +877,22 @@ export function registerGoalLifecycle(
|
|
|
582
877
|
}
|
|
583
878
|
if (!dispatchedQueueAction) runtime.dispatchContinuationIfSettled(ctx);
|
|
584
879
|
runtime.clearSettledSafetyTracking();
|
|
880
|
+
runtime.acceptedRunPrompt = undefined;
|
|
881
|
+
runtime.pendingInputWake = undefined;
|
|
882
|
+
runtime.inputWakeGoalId = undefined;
|
|
883
|
+
runtime.inputWakeNeedsPrompt = false;
|
|
884
|
+
runtime.scheduleGoalWaitWake(ctx);
|
|
585
885
|
});
|
|
586
886
|
|
|
587
|
-
function beginNonGoalFollowUp(ctx: StatusContext,
|
|
887
|
+
function beginNonGoalFollowUp(ctx: StatusContext, realInput = false, inRun = false) {
|
|
888
|
+
const resumed = realInput && commands.resumeWaitingGoalOnInput(ctx, inRun);
|
|
889
|
+
if (resumed) runtime.guardAbortGoalId = undefined;
|
|
588
890
|
runtime.clearGoalRecovery();
|
|
589
891
|
runtime.clearStaleGoalToolCallBlock();
|
|
590
|
-
if (
|
|
892
|
+
if (realInput) runtime.clearBudgetWrapUp();
|
|
591
893
|
const activeGoalId = runtime.activeGoal?.status === "active" ? runtime.activeGoal.id : undefined;
|
|
592
894
|
runtime.beginAgentRun(activeGoalId ?? null, activeGoalId ? "manual" : undefined);
|
|
593
|
-
if (
|
|
895
|
+
if (realInput && activeGoalId && !resumed) runtime.resetActiveSafetyEpoch(ctx);
|
|
594
896
|
}
|
|
595
897
|
|
|
596
898
|
function stopGoalAfterAgentEnd(
|
package/src/menu.ts
CHANGED
|
@@ -597,7 +597,7 @@ function goalMainMenuItem(label: string): ActionMenuItem<GoalMenuScreen, GoalMen
|
|
|
597
597
|
|
|
598
598
|
function refreshGoalMenuState(runtime: GoalMenuRuntimeView, ctx: ExtensionCommandContext) {
|
|
599
599
|
const goal = runtime.activeGoal;
|
|
600
|
-
if (!goal || runtime.queueFrozen) return;
|
|
600
|
+
if (!goal || runtime.queueFrozen || goal.status !== "active") return;
|
|
601
601
|
runtime.recordGoalUsage?.(goal, ctx);
|
|
602
602
|
runtime.persistGoal?.(goal);
|
|
603
603
|
runtime.updateStatus?.(ctx, goal);
|
package/src/persistence.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { isNonNegativeFiniteNumber, nonNegativeFiniteNumber, normalizeTokenBudget } from "./accounting.js";
|
|
6
6
|
import type { GoalStatus } from "./prompts.js";
|
|
7
|
+
import { type GoalWait, normalizeGoalWait } from "./wait.js";
|
|
7
8
|
|
|
8
9
|
const GOAL_STATE_ENTRY_TYPE = "goal-state";
|
|
9
10
|
const LEGACY_GOALS_STATE_ENTRY_TYPE = "goals-state";
|
|
@@ -22,12 +23,17 @@ export interface ActiveGoal {
|
|
|
22
23
|
tokensUsed: number;
|
|
23
24
|
timeUsedSeconds: number;
|
|
24
25
|
baselineTokens: number;
|
|
26
|
+
usageOffset?: number;
|
|
27
|
+
/** A preceding run still owns usage after this activation checkpoint. */
|
|
28
|
+
usageBaselinePending?: boolean;
|
|
25
29
|
activeStartedAt?: number;
|
|
26
30
|
automaticModelTurns: number;
|
|
27
31
|
toolFreeRepeatCount: number;
|
|
28
32
|
lastToolFreeOutputFingerprint?: string;
|
|
29
33
|
safetyPauseCause?: SafetyPauseCause;
|
|
30
34
|
safetyResetPending?: boolean;
|
|
35
|
+
/** Only a paused goal with this record may wake without /goal resume. */
|
|
36
|
+
wait?: GoalWait;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export type PendingQueueAction =
|
|
@@ -218,12 +224,18 @@ export function normalizeLoadedGoal(goal: ActiveGoal): ActiveGoal {
|
|
|
218
224
|
tokensUsed: nonNegativeFiniteNumber(goal.tokensUsed),
|
|
219
225
|
timeUsedSeconds: nonNegativeFiniteNumber(goal.timeUsedSeconds),
|
|
220
226
|
baselineTokens: nonNegativeFiniteNumber(goal.baselineTokens),
|
|
227
|
+
usageOffset:
|
|
228
|
+
isNonNegativeFiniteNumber(goal.usageOffset) && goal.usageOffset <= nonNegativeFiniteNumber(goal.tokensUsed)
|
|
229
|
+
? goal.usageOffset
|
|
230
|
+
: undefined,
|
|
231
|
+
usageBaselinePending: goal.usageBaselinePending === true ? true : undefined,
|
|
221
232
|
activeStartedAt: goal.status === "active" ? now : undefined,
|
|
222
233
|
automaticModelTurns: normalizeSafetyCounter(goal.automaticModelTurns),
|
|
223
234
|
toolFreeRepeatCount: normalizeSafetyCounter(goal.toolFreeRepeatCount),
|
|
224
235
|
lastToolFreeOutputFingerprint: normalizeOutputFingerprint(goal.lastToolFreeOutputFingerprint),
|
|
225
236
|
safetyPauseCause: normalizeSafetyPauseCause(goal.safetyPauseCause),
|
|
226
237
|
safetyResetPending: goal.safetyResetPending === true ? true : undefined,
|
|
238
|
+
wait: goal.status === "paused" && !goal.safetyPauseCause ? normalizeGoalWait(goal.wait) : undefined,
|
|
227
239
|
};
|
|
228
240
|
}
|
|
229
241
|
|
package/src/prompts.ts
CHANGED
|
@@ -34,7 +34,14 @@ export function buildResumePrompt(goal: GoalPromptContext, stoppedStatus: GoalSt
|
|
|
34
34
|
export function buildGoalSystemPrompt(goal: GoalPromptContext) {
|
|
35
35
|
const budgetLine =
|
|
36
36
|
goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
|
|
37
|
-
return `Active /goal:\n${goalContextBlock(goal)}\n\n${goalModeRules("the active goal")}${budgetLine}`;
|
|
37
|
+
return `Active /goal:\n${goalContextBlock(goal)}\n\nA later Goal runtime binding update can replace this goal_id after real input resumes a wait. Use its current goal_id, not the earlier binding.\n\n${goalModeRules("the active goal")}${budgetLine}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const GOAL_BINDING_UPDATE_HEADER =
|
|
41
|
+
"Goal runtime binding update: real input resumed the waiting goal. This current goal_id supersedes the earlier binding.";
|
|
42
|
+
|
|
43
|
+
export function buildGoalBindingUpdate(goal: GoalPromptContext) {
|
|
44
|
+
return `${GOAL_BINDING_UPDATE_HEADER}\n\n${goalCompletionGuardBlock(goal)}`;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
export function buildContinuePrompt(goal: GoalPromptContext, marker: string) {
|
package/src/queue.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { checkpointGoalActiveTime } from "./accounting.js";
|
|
2
|
+
import { checkpointGoalActiveTime, rebaseGoalUsage } from "./accounting.js";
|
|
3
3
|
import type { ActiveGoal, PendingQueueAction } from "./persistence.js";
|
|
4
4
|
import { resetGoalSafetyEpoch } from "./safety.js";
|
|
5
5
|
|
|
@@ -83,8 +83,7 @@ export function shelveGoal(goal: ActiveGoal, now = Date.now()): ActiveGoal {
|
|
|
83
83
|
|
|
84
84
|
export function activateQueuedGoal(goal: ActiveGoal, currentTokenTotal: number, now = Date.now()): ActiveGoal {
|
|
85
85
|
const rebased = {
|
|
86
|
-
...goal,
|
|
87
|
-
baselineTokens: Math.max(0, currentTokenTotal - goal.tokensUsed),
|
|
86
|
+
...rebaseGoalUsage(goal, currentTokenTotal),
|
|
88
87
|
activeStartedAt: undefined,
|
|
89
88
|
updatedAt: now,
|
|
90
89
|
};
|
package/src/run-protocol.ts
CHANGED
|
@@ -169,6 +169,21 @@ export class GoalRunController {
|
|
|
169
169
|
this.usedRunIds.clear();
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
handleTreeNavigation(goalId: string | undefined) {
|
|
173
|
+
const run = this.run;
|
|
174
|
+
if (!run || run.closed || run.goalId === goalId) return;
|
|
175
|
+
if (run.goalId) {
|
|
176
|
+
this.publishStateEvent(run, {
|
|
177
|
+
goalId: run.goalId,
|
|
178
|
+
status: "cleared",
|
|
179
|
+
reason: "managed Goal left by session tree navigation",
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.closeCurrentRun();
|
|
184
|
+
this.emitError(run.runId, "start", "SUPERSEDED", "Goal run superseded by session tree navigation.");
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
private async handleStart(data: unknown) {
|
|
173
188
|
const runId = parseRunId(data);
|
|
174
189
|
if (!runId) return;
|