pi-better-goal 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/index.ts +75 -37
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
13
13
|
## Core Features
|
|
14
14
|
|
|
15
15
|
- `/goal` runtime for starting, pausing, resuming, completing, and clearing the current objective.
|
|
16
|
+
- `escape` pauses the active goal; paused goals are never poked.
|
|
16
17
|
- A compact goal widget that does not replace Pi's footer.
|
|
17
18
|
- Background activity tracking for subagents and other registered providers.
|
|
18
19
|
- A progress-aware follow-up loop that holds after repeated identical outcomes.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
goalWithStatus,
|
|
20
20
|
validateObjective,
|
|
21
21
|
validateTokenBudget,
|
|
22
|
+
type GoalEntrySource,
|
|
22
23
|
} from "./goal-state.js";
|
|
23
24
|
import { continuationEvidence, type ContinuationEvidence } from "./continuation.js";
|
|
24
25
|
import { observeGoalStall } from "./stall.js";
|
|
@@ -73,6 +74,26 @@ function parseRetryLimit(raw: string | undefined, fallback: number): number {
|
|
|
73
74
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* True when the agent loop ended because the running turn was interrupted
|
|
79
|
+
* (escape / ctrl+c). An aborted run leaves a final assistant message whose
|
|
80
|
+
* stopReason is "aborted".
|
|
81
|
+
*/
|
|
82
|
+
function wasTurnAborted(messages: readonly unknown[]): boolean {
|
|
83
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
84
|
+
const message = messages[index];
|
|
85
|
+
if (!message || typeof message !== "object") {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const candidate = message as { role?: unknown; stopReason?: unknown };
|
|
89
|
+
if (candidate.role !== "assistant") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return candidate.stopReason === "aborted";
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
function continuationPrompt(goal: GoalSnapshot): string {
|
|
77
98
|
return [
|
|
78
99
|
"Continue working toward the active thread goal.",
|
|
@@ -148,6 +169,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
148
169
|
|
|
149
170
|
const getGoal = (ctx: ExtensionContext): GoalSnapshot | null => currentGoalSnapshot(ctx);
|
|
150
171
|
|
|
172
|
+
/** Only active goals receive autonomous pokes; paused, complete, and budget-limited goals never do. */
|
|
173
|
+
const isPokeable = (goal: GoalSnapshot | null): goal is GoalSnapshot => goal?.status === "active";
|
|
174
|
+
|
|
151
175
|
const appendContinuationState = (state: ReturnType<typeof createContinuationState>): void => {
|
|
152
176
|
pi.appendEntry(EXTENSION_NAME, continuationStateEntry(state));
|
|
153
177
|
};
|
|
@@ -156,7 +180,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
156
180
|
appendContinuationState(createContinuationState(goal.goalId));
|
|
157
181
|
};
|
|
158
182
|
|
|
159
|
-
const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source:
|
|
183
|
+
const setGoal = (goal: GoalSnapshot, ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
160
184
|
pi.appendEntry(EXTENSION_NAME, goalSetEntry(goal, source));
|
|
161
185
|
continuationQueuedFor = null;
|
|
162
186
|
backgroundDrainTracker = null;
|
|
@@ -164,7 +188,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
188
|
refreshGoalWidget?.();
|
|
165
189
|
};
|
|
166
190
|
|
|
167
|
-
const clearGoal = (ctx: ExtensionContext, source:
|
|
191
|
+
const clearGoal = (ctx: ExtensionContext, source: GoalEntrySource): void => {
|
|
168
192
|
const current = getGoal(ctx);
|
|
169
193
|
pi.appendEntry(EXTENSION_NAME, goalClearEntry(current?.goalId ?? null, source));
|
|
170
194
|
continuationQueuedFor = null;
|
|
@@ -174,7 +198,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
174
198
|
};
|
|
175
199
|
|
|
176
200
|
const queueGoalContinuation = (goal: GoalSnapshot): void => {
|
|
177
|
-
if (goal
|
|
201
|
+
if (!isPokeable(goal) || continuationQueuedFor === goal.goalId) {
|
|
178
202
|
return;
|
|
179
203
|
}
|
|
180
204
|
clearIdleContinuation();
|
|
@@ -205,7 +229,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
205
229
|
kind: "continuation" | "background-drained",
|
|
206
230
|
snapshot?: ActivitySnapshot,
|
|
207
231
|
): void => {
|
|
208
|
-
if (WAKE_DISABLED || goal
|
|
232
|
+
if (WAKE_DISABLED || !isPokeable(goal) || continuationQueuedFor === goal.goalId) {
|
|
209
233
|
return;
|
|
210
234
|
}
|
|
211
235
|
if (kind === "continuation" && currentContinuationState(ctx, goal.goalId)?.blocked) {
|
|
@@ -232,7 +256,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
232
256
|
priorSnapshot?: ActivitySnapshot,
|
|
233
257
|
): Promise<void> => {
|
|
234
258
|
const goal = currentGoalSnapshot(ctx);
|
|
235
|
-
if (goal
|
|
259
|
+
if (!isPokeable(goal) || goal.goalId !== goalId || continuationQueuedFor === goal.goalId || foregroundRunning) {
|
|
236
260
|
return;
|
|
237
261
|
}
|
|
238
262
|
const snapshot = await collectActivitySnapshot(ctx, providers.values(), foregroundRunning);
|
|
@@ -299,7 +323,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
299
323
|
const goal = currentGoalSnapshot(ctx);
|
|
300
324
|
const wakePlan = planBackgroundDrainWake(backgroundDrainTracker, goal, snapshot);
|
|
301
325
|
backgroundDrainTracker = wakePlan.nextTracker;
|
|
302
|
-
if (goal
|
|
326
|
+
if (isPokeable(goal) && wakePlan.wakeSignature) {
|
|
303
327
|
const wakeSignature = wakePlan.wakeSignature;
|
|
304
328
|
if (wakeSignature !== lastWakeSignature) {
|
|
305
329
|
lastWakeSignature = wakeSignature;
|
|
@@ -586,7 +610,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
586
610
|
currentCtx = ctx;
|
|
587
611
|
const goal = currentGoalSnapshot(ctx);
|
|
588
612
|
const snapshot = await publishSnapshot(ctx);
|
|
589
|
-
if (goal
|
|
613
|
+
if (!isPokeable(goal)) {
|
|
590
614
|
return;
|
|
591
615
|
}
|
|
592
616
|
|
|
@@ -609,8 +633,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
609
633
|
await publishSnapshot(ctx);
|
|
610
634
|
});
|
|
611
635
|
|
|
612
|
-
pi.on("agent_end", async (event,
|
|
636
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
613
637
|
lastAgentEvidence = continuationEvidence(event.messages);
|
|
638
|
+
// `escape` is a pi-reserved built-in shortcut (`app.interrupt`), so extensions
|
|
639
|
+
// cannot register it. Observe the interrupt instead: when a running turn is
|
|
640
|
+
// aborted (escape / ctrl+c while streaming), pause the active goal so it does
|
|
641
|
+
// not auto-continue after the user stopped the agent.
|
|
642
|
+
if (wasTurnAborted(event.messages)) {
|
|
643
|
+
const goal = getGoal(ctx);
|
|
644
|
+
if (isPokeable(goal)) {
|
|
645
|
+
setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
646
|
+
if (ctx.hasUI) {
|
|
647
|
+
ctx.ui.notify("Goal paused (interrupted).", "info");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
614
651
|
});
|
|
615
652
|
|
|
616
653
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
@@ -618,37 +655,38 @@ export default function (pi: ExtensionAPI): void {
|
|
|
618
655
|
foregroundRunning = false;
|
|
619
656
|
const snapshot = await publishSnapshot(ctx);
|
|
620
657
|
const goal = getGoal(ctx);
|
|
621
|
-
if (goal
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
|
|
658
|
+
if (!isPokeable(goal) || snapshot.backgroundRunning) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const previous = currentContinuationState(ctx, goal.goalId) ?? createContinuationState(goal.goalId);
|
|
662
|
+
const evidence = lastAgentEvidence ?? continuationEvidence([]);
|
|
663
|
+
const repeated = previous.lastEvidenceSignature === evidence.signature;
|
|
664
|
+
const noProgressRetries = repeated ? previous.noProgressRetries + 1 : 0;
|
|
665
|
+
const blocked = repeated && noProgressRetries >= MAX_NO_PROGRESS_RETRIES;
|
|
666
|
+
const now = Date.now();
|
|
667
|
+
appendContinuationState({
|
|
668
|
+
goalId: goal.goalId,
|
|
669
|
+
lastEvidenceSignature: evidence.signature,
|
|
670
|
+
lastEvidenceSummary: evidence.summary,
|
|
671
|
+
...(repeated
|
|
672
|
+
? (previous.lastProgressAt !== undefined ? { lastProgressAt: previous.lastProgressAt } : {})
|
|
673
|
+
: { lastProgressAt: now }),
|
|
674
|
+
noProgressRetries,
|
|
675
|
+
blocked,
|
|
676
|
+
updatedAt: Math.floor(now / 1000),
|
|
677
|
+
});
|
|
678
|
+
if (blocked) {
|
|
679
|
+
refreshGoalWidget?.();
|
|
680
|
+
if (ctx.hasUI) {
|
|
681
|
+
ctx.ui.setStatus(EXTENSION_NAME, "waiting: no progress");
|
|
682
|
+
ctx.ui.notify(
|
|
683
|
+
`Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). New input or background activity will resume it.`,
|
|
684
|
+
"warning",
|
|
685
|
+
);
|
|
649
686
|
}
|
|
650
|
-
|
|
687
|
+
return;
|
|
651
688
|
}
|
|
689
|
+
scheduleIdleContinuation(goal, ctx, "continuation", snapshot);
|
|
652
690
|
});
|
|
653
691
|
|
|
654
692
|
pi.on("session_shutdown", async (_event, ctx) => {
|