@kky42/pi-goal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/package.json +73 -0
- package/src/commands.ts +107 -0
- package/src/continuation-scheduler.ts +174 -0
- package/src/format.ts +232 -0
- package/src/goal-accounting.ts +128 -0
- package/src/goal-persistence.ts +73 -0
- package/src/goal-runtime-agent-handlers.ts +51 -0
- package/src/goal-runtime-controller.ts +162 -0
- package/src/goal-runtime-event-handler-types.ts +166 -0
- package/src/goal-runtime-event-handlers.ts +31 -0
- package/src/goal-runtime-event-utils.ts +93 -0
- package/src/goal-runtime-events.ts +24 -0
- package/src/goal-runtime-input-context-handlers.ts +144 -0
- package/src/goal-runtime-session-handlers.ts +131 -0
- package/src/goal-runtime-state.ts +22 -0
- package/src/goal-runtime-status.ts +62 -0
- package/src/goal-runtime-turn-handlers.ts +66 -0
- package/src/goal-state-controller.ts +210 -0
- package/src/goal-transition-effects.ts +91 -0
- package/src/goal-transition.ts +396 -0
- package/src/index.ts +9 -0
- package/src/prompts.ts +170 -0
- package/src/queued-goal-messages.ts +166 -0
- package/src/queued-goal-work.ts +96 -0
- package/src/recovery-adapters.ts +66 -0
- package/src/recovery-machine.ts +196 -0
- package/src/recovery-phase.ts +95 -0
- package/src/recovery-runtime.ts +97 -0
- package/src/recovery.ts +151 -0
- package/src/runtime-config.ts +7 -0
- package/src/stale-queued-work-guard.ts +114 -0
- package/src/stale-queued-work-obligations.ts +291 -0
- package/src/stale-queued-work-reducer.ts +483 -0
- package/src/stale-queued-work-terminal-cleanup.ts +84 -0
- package/src/stale-queued-work-types.ts +81 -0
- package/src/state.ts +404 -0
- package/src/tools.ts +101 -0
- package/src/types.ts +60 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionHandler,
|
|
3
|
+
TurnEndEvent,
|
|
4
|
+
TurnStartEvent,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { assistantTurnTokens, isAbortedAssistantMessage, isToolUseAssistantMessage } from "./goal-accounting.js";
|
|
8
|
+
import { isAssistantContextOverflow, isErrorAssistantMessage } from "./recovery.js";
|
|
9
|
+
import { getContextWindow, runStaleQueuedWorkPlan } from "./goal-runtime-event-utils.js";
|
|
10
|
+
import type {
|
|
11
|
+
GoalRuntimeTurnHandlerContext,
|
|
12
|
+
ToolExecutionEndEvent,
|
|
13
|
+
} from "./goal-runtime-event-handler-types.js";
|
|
14
|
+
|
|
15
|
+
export function createTurnEventHandlers(deps: GoalRuntimeTurnHandlerContext) {
|
|
16
|
+
const { runtimeState, stateController, continuation, goalAccounting, recoveryRuntime, status } = deps;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
onTurnStart: (async (event, ctx) => {
|
|
20
|
+
runtimeState.currentTurnIndex = event.turnIndex;
|
|
21
|
+
continuation.bindPassthroughContinuationInputToTurn(event.turnIndex);
|
|
22
|
+
runStaleQueuedWorkPlan(runtimeState.staleQueuedWorkGuard.planTurnStart(), ctx, deps);
|
|
23
|
+
goalAccounting.beginAccounting();
|
|
24
|
+
status.refreshUi(ctx);
|
|
25
|
+
}) satisfies ExtensionHandler<TurnStartEvent>,
|
|
26
|
+
|
|
27
|
+
onToolExecutionEnd: (async (_event, ctx) => {
|
|
28
|
+
if (runStaleQueuedWorkPlan(runtimeState.staleQueuedWorkGuard.planToolExecutionEnd(), ctx, deps)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
goalAccounting.accountProgress(ctx, true, 0, true);
|
|
33
|
+
stateController.maybeFlushRuntimePersistence("runtime");
|
|
34
|
+
}) satisfies ExtensionHandler<ToolExecutionEndEvent>,
|
|
35
|
+
|
|
36
|
+
onTurnEnd: (async (event, ctx) => {
|
|
37
|
+
if (
|
|
38
|
+
runStaleQueuedWorkPlan(
|
|
39
|
+
runtimeState.staleQueuedWorkGuard.planTurnEnd(event.turnIndex),
|
|
40
|
+
ctx,
|
|
41
|
+
deps,
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const completedTurnTokens = assistantTurnTokens(event.message);
|
|
48
|
+
goalAccounting.accountProgress(ctx, true, completedTurnTokens);
|
|
49
|
+
stateController.flushGoalPersistence("runtime");
|
|
50
|
+
if (isAbortedAssistantMessage(event.message)) {
|
|
51
|
+
stateController.pauseForAbort(ctx);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (isErrorAssistantMessage(event.message)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (isAssistantContextOverflow(event.message, getContextWindow(ctx))) {
|
|
58
|
+
stateController.beginOverflowRecovery(ctx);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
recoveryRuntime.finishSuccessfulAssistantTurn(event.message, ctx, {
|
|
62
|
+
continueGoal: !isToolUseAssistantMessage(event.message),
|
|
63
|
+
});
|
|
64
|
+
}) satisfies ExtensionHandler<TurnEndEvent>,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { GoalPersistence } from "./goal-persistence.js";
|
|
4
|
+
import type { StatusContext } from "./goal-runtime-status.js";
|
|
5
|
+
import {
|
|
6
|
+
applyGoalTransitionEffects,
|
|
7
|
+
planGoalTransition,
|
|
8
|
+
type GoalTransitionEffect,
|
|
9
|
+
type GoalTransitionEffectHandlers,
|
|
10
|
+
type GoalTransitionRequest,
|
|
11
|
+
} from "./goal-transition.js";
|
|
12
|
+
import {
|
|
13
|
+
applyHostOverflowUserResetPersistence,
|
|
14
|
+
beginHostOverflowRecovery,
|
|
15
|
+
requireHostOverflowUserReset,
|
|
16
|
+
syncHostOverflowUserResetFromSession,
|
|
17
|
+
type GoalRecoveryMachineState,
|
|
18
|
+
} from "./recovery-machine.js";
|
|
19
|
+
import {
|
|
20
|
+
goalsEquivalent,
|
|
21
|
+
hostOverflowCapResetEntry,
|
|
22
|
+
reconstructGoal,
|
|
23
|
+
reconstructHostOverflowCapNeedsUserReset,
|
|
24
|
+
updateGoalStatus,
|
|
25
|
+
} from "./state.js";
|
|
26
|
+
import { CUSTOM_ENTRY_TYPE, type GoalEntrySource, type GoalResult, type ThreadGoal } from "./types.js";
|
|
27
|
+
|
|
28
|
+
interface GoalStateControllerDeps {
|
|
29
|
+
pi: Pick<ExtensionAPI, "appendEntry">;
|
|
30
|
+
persistence: GoalPersistence;
|
|
31
|
+
getRecoveryState: () => GoalRecoveryMachineState;
|
|
32
|
+
transitionEffectHandlers: GoalTransitionEffectHandlers;
|
|
33
|
+
refreshUi: (ctx: StatusContext) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function reloadRuntimeEffects(
|
|
37
|
+
previousGoalId: string | null,
|
|
38
|
+
reconstructed: ThreadGoal | null,
|
|
39
|
+
): GoalTransitionEffect[] {
|
|
40
|
+
const effects: GoalTransitionEffect[] = [{ type: "clearContinuation" }];
|
|
41
|
+
if (reconstructed?.status !== "active") {
|
|
42
|
+
effects.push({ type: "clearActiveAccounting" });
|
|
43
|
+
}
|
|
44
|
+
if ((reconstructed?.goalId ?? null) !== previousGoalId) {
|
|
45
|
+
effects.push({ type: "resetRecovery" });
|
|
46
|
+
}
|
|
47
|
+
return effects;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GoalStateController {
|
|
51
|
+
applyGoalTransition: (
|
|
52
|
+
request: GoalTransitionRequest,
|
|
53
|
+
ctx: StatusContext | null,
|
|
54
|
+
) => boolean;
|
|
55
|
+
beginOverflowRecovery: (ctx: StatusContext) => void;
|
|
56
|
+
updateGoal: (status: "complete" | "blocked", source: GoalEntrySource, ctx: ExtensionContext) => GoalResult;
|
|
57
|
+
flushGoalPersistence: GoalPersistence["flushGoalPersistence"];
|
|
58
|
+
getGoal: () => ThreadGoal | null;
|
|
59
|
+
isCurrentActiveGoalId: (goalId: string) => boolean;
|
|
60
|
+
maybeFlushRuntimePersistence: GoalPersistence["maybeFlushRuntimePersistence"];
|
|
61
|
+
pauseForAbort: (ctx: ExtensionContext) => void;
|
|
62
|
+
persistHostOverflowUserReset: (needsReset: boolean) => void;
|
|
63
|
+
reloadFromSession: (ctx: ExtensionContext) => void;
|
|
64
|
+
resumePausedGoal: (ctx: ExtensionContext) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createGoalStateController(deps: GoalStateControllerDeps) {
|
|
68
|
+
const getGoal = (): ThreadGoal | null => deps.persistence.getGoal();
|
|
69
|
+
|
|
70
|
+
const isCurrentActiveGoalId = (goalId: string): boolean =>
|
|
71
|
+
getGoal()?.goalId === goalId && getGoal()?.status === "active";
|
|
72
|
+
|
|
73
|
+
const applyGoalTransition = (
|
|
74
|
+
request: GoalTransitionRequest,
|
|
75
|
+
ctx: StatusContext | null,
|
|
76
|
+
): boolean => {
|
|
77
|
+
const plan = planGoalTransition(getGoal(), request);
|
|
78
|
+
|
|
79
|
+
applyGoalTransitionEffects(plan.beforePersist, deps.transitionEffectHandlers);
|
|
80
|
+
|
|
81
|
+
if (plan.persist === "clear") {
|
|
82
|
+
const clearedGoalId = getGoal()?.goalId ?? null;
|
|
83
|
+
deps.persistence.appendClearEntry(clearedGoalId, plan.source);
|
|
84
|
+
applyGoalTransitionEffects(plan.afterPersist, deps.transitionEffectHandlers);
|
|
85
|
+
if (ctx) {
|
|
86
|
+
deps.refreshUi(ctx);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (plan.persist === "skip") {
|
|
92
|
+
applyGoalTransitionEffects(plan.afterPersist, deps.transitionEffectHandlers);
|
|
93
|
+
if (ctx) {
|
|
94
|
+
deps.refreshUi(ctx);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (plan.persist === "defer") {
|
|
100
|
+
deps.persistence.setGoalSnapshot(plan.nextGoal);
|
|
101
|
+
if (ctx) {
|
|
102
|
+
deps.refreshUi(ctx);
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
deps.persistence.setGoalSnapshot(plan.nextGoal);
|
|
108
|
+
const persisted = deps.persistence.flushGoalPersistence(plan.source);
|
|
109
|
+
applyGoalTransitionEffects(plan.afterPersist, deps.transitionEffectHandlers);
|
|
110
|
+
if (ctx) {
|
|
111
|
+
deps.refreshUi(ctx);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return persisted;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const persistHostOverflowUserReset = (needsReset: boolean): void => {
|
|
118
|
+
if (!applyHostOverflowUserResetPersistence(deps.getRecoveryState(), needsReset)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
deps.pi.appendEntry(CUSTOM_ENTRY_TYPE, hostOverflowCapResetEntry(needsReset));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const beginOverflowRecovery = (ctx: StatusContext): void => {
|
|
125
|
+
const goal = getGoal();
|
|
126
|
+
const hasActiveGoal = Boolean(goal && goal.status === "active");
|
|
127
|
+
let shouldPersist: boolean;
|
|
128
|
+
|
|
129
|
+
if (hasActiveGoal) {
|
|
130
|
+
applyGoalTransitionEffects([{ type: "clearContinuation" }], deps.transitionEffectHandlers);
|
|
131
|
+
const { persistHostOverflowCapReset } = beginHostOverflowRecovery(deps.getRecoveryState());
|
|
132
|
+
shouldPersist = persistHostOverflowCapReset;
|
|
133
|
+
deps.refreshUi(ctx);
|
|
134
|
+
} else {
|
|
135
|
+
shouldPersist = requireHostOverflowUserReset(deps.getRecoveryState());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (shouldPersist) {
|
|
139
|
+
deps.pi.appendEntry(CUSTOM_ENTRY_TYPE, hostOverflowCapResetEntry(true));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const reloadFromSession = (ctx: ExtensionContext): void => {
|
|
144
|
+
const previousGoalId = getGoal()?.goalId ?? null;
|
|
145
|
+
const branch = ctx.sessionManager.getBranch();
|
|
146
|
+
const reconstructed = reconstructGoal(branch).goal;
|
|
147
|
+
deps.persistence.setGoalSnapshot(reconstructed);
|
|
148
|
+
deps.persistence.syncPersistedSnapshot(reconstructed);
|
|
149
|
+
syncHostOverflowUserResetFromSession(
|
|
150
|
+
deps.getRecoveryState(),
|
|
151
|
+
reconstructHostOverflowCapNeedsUserReset(branch),
|
|
152
|
+
);
|
|
153
|
+
applyGoalTransitionEffects(
|
|
154
|
+
reloadRuntimeEffects(previousGoalId, reconstructed),
|
|
155
|
+
deps.transitionEffectHandlers,
|
|
156
|
+
);
|
|
157
|
+
deps.refreshUi(ctx);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const pauseForAbort = (ctx: ExtensionContext): void => {
|
|
161
|
+
const goal = getGoal();
|
|
162
|
+
if (!goal || goal.status !== "active") {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
applyGoalTransition({ kind: "abort_pause" }, ctx);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const resumePausedGoal = (ctx: ExtensionContext): void => {
|
|
170
|
+
const goal = getGoal();
|
|
171
|
+
if (!goal || (goal.status !== "paused" && goal.status !== "blocked")) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
applyGoalTransition({ kind: "resume_active" }, ctx);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const updateGoal = (
|
|
179
|
+
status: "complete" | "blocked",
|
|
180
|
+
source: GoalEntrySource,
|
|
181
|
+
ctx: ExtensionContext,
|
|
182
|
+
): GoalResult => {
|
|
183
|
+
const goal = getGoal();
|
|
184
|
+
const result = updateGoalStatus(goal, status);
|
|
185
|
+
if (!result.ok || !result.goal) {
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
if (goal && goalsEquivalent(goal, result.goal)) {
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
applyGoalTransition({ kind: "set", nextGoal: result.goal, source }, ctx);
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const controller: GoalStateController = {
|
|
196
|
+
applyGoalTransition,
|
|
197
|
+
beginOverflowRecovery,
|
|
198
|
+
updateGoal,
|
|
199
|
+
flushGoalPersistence: deps.persistence.flushGoalPersistence,
|
|
200
|
+
getGoal,
|
|
201
|
+
isCurrentActiveGoalId,
|
|
202
|
+
maybeFlushRuntimePersistence: deps.persistence.maybeFlushRuntimePersistence,
|
|
203
|
+
pauseForAbort,
|
|
204
|
+
persistHostOverflowUserReset,
|
|
205
|
+
reloadFromSession,
|
|
206
|
+
resumePausedGoal,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return controller;
|
|
210
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type GoalTransitionEffect =
|
|
2
|
+
| { type: "clearContinuation" }
|
|
3
|
+
| { type: "clearActiveAccounting" }
|
|
4
|
+
| { type: "resetRecovery" }
|
|
5
|
+
| { type: "clearBudgetWarning" }
|
|
6
|
+
| { type: "clearHostOverflowRecovery" }
|
|
7
|
+
| { type: "setRecoveryPausedAttention"; reason: string }
|
|
8
|
+
| { type: "markContinuationQueued"; goalId: string }
|
|
9
|
+
| { type: "stopStatusRefresh" };
|
|
10
|
+
|
|
11
|
+
export interface GoalTransitionEffectHandlers {
|
|
12
|
+
clearContinuation: () => void;
|
|
13
|
+
clearActiveAccounting: () => void;
|
|
14
|
+
resetRecovery: () => void;
|
|
15
|
+
clearBudgetWarning: () => void;
|
|
16
|
+
clearHostOverflowRecovery: () => void;
|
|
17
|
+
setRecoveryPausedAttention: (reason: string) => void;
|
|
18
|
+
markContinuationQueued: (goalId: string) => void;
|
|
19
|
+
stopStatusRefresh: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function goalTransitionEffectKey(effect: GoalTransitionEffect): string {
|
|
23
|
+
switch (effect.type) {
|
|
24
|
+
case "setRecoveryPausedAttention":
|
|
25
|
+
return `${effect.type}:${effect.reason}`;
|
|
26
|
+
case "markContinuationQueued":
|
|
27
|
+
return `${effect.type}:${effect.goalId}`;
|
|
28
|
+
default:
|
|
29
|
+
return effect.type;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function appendGoalTransitionEffectOnce(
|
|
34
|
+
effects: GoalTransitionEffect[],
|
|
35
|
+
effect: GoalTransitionEffect,
|
|
36
|
+
): void {
|
|
37
|
+
const key = goalTransitionEffectKey(effect);
|
|
38
|
+
if (!effects.some((existing) => goalTransitionEffectKey(existing) === key)) {
|
|
39
|
+
effects.push(effect);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergeGoalTransitionEffects(
|
|
44
|
+
...groups: readonly GoalTransitionEffect[][]
|
|
45
|
+
): GoalTransitionEffect[] {
|
|
46
|
+
const result: GoalTransitionEffect[] = [];
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
for (const effect of group) {
|
|
49
|
+
appendGoalTransitionEffectOnce(result, effect);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function applyGoalTransitionEffects(
|
|
56
|
+
effects: readonly GoalTransitionEffect[],
|
|
57
|
+
handlers: GoalTransitionEffectHandlers,
|
|
58
|
+
): void {
|
|
59
|
+
for (const effect of effects) {
|
|
60
|
+
switch (effect.type) {
|
|
61
|
+
case "clearContinuation":
|
|
62
|
+
handlers.clearContinuation();
|
|
63
|
+
break;
|
|
64
|
+
case "clearActiveAccounting":
|
|
65
|
+
handlers.clearActiveAccounting();
|
|
66
|
+
break;
|
|
67
|
+
case "resetRecovery":
|
|
68
|
+
handlers.resetRecovery();
|
|
69
|
+
break;
|
|
70
|
+
case "clearBudgetWarning":
|
|
71
|
+
handlers.clearBudgetWarning();
|
|
72
|
+
break;
|
|
73
|
+
case "clearHostOverflowRecovery":
|
|
74
|
+
handlers.clearHostOverflowRecovery();
|
|
75
|
+
break;
|
|
76
|
+
case "setRecoveryPausedAttention":
|
|
77
|
+
handlers.setRecoveryPausedAttention(effect.reason);
|
|
78
|
+
break;
|
|
79
|
+
case "markContinuationQueued":
|
|
80
|
+
handlers.markContinuationQueued(effect.goalId);
|
|
81
|
+
break;
|
|
82
|
+
case "stopStatusRefresh":
|
|
83
|
+
handlers.stopStatusRefresh();
|
|
84
|
+
break;
|
|
85
|
+
default: {
|
|
86
|
+
const _exhaustive: never = effect;
|
|
87
|
+
throw new Error(`Unhandled goal transition effect: ${String(_exhaustive)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|