@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,166 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentEndEvent,
|
|
3
|
+
BeforeAgentStartEvent,
|
|
4
|
+
ContextEvent,
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
ExtensionEvent,
|
|
8
|
+
ExtensionHandler,
|
|
9
|
+
InputEvent,
|
|
10
|
+
InputEventResult,
|
|
11
|
+
SessionBeforeCompactEvent,
|
|
12
|
+
SessionCompactEvent,
|
|
13
|
+
SessionShutdownEvent,
|
|
14
|
+
SessionStartEvent,
|
|
15
|
+
SessionTreeEvent,
|
|
16
|
+
TurnEndEvent,
|
|
17
|
+
TurnStartEvent,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
import type { GoalRuntimeState } from "./goal-runtime-state.js";
|
|
21
|
+
import type { GoalStateController } from "./goal-state-controller.js";
|
|
22
|
+
import type { AssistantErrorMessage } from "./recovery.js";
|
|
23
|
+
|
|
24
|
+
export type ContextEventResult = { messages?: ContextEvent["messages"] };
|
|
25
|
+
export type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
26
|
+
export type ToolExecutionEndEvent = Extract<ExtensionEvent, { type: "tool_execution_end" }>;
|
|
27
|
+
|
|
28
|
+
export interface GoalRuntimeEventHandlers {
|
|
29
|
+
onInput: ExtensionHandler<InputEvent, InputEventResult>;
|
|
30
|
+
onContext: ExtensionHandler<ContextEvent, ContextEventResult | undefined>;
|
|
31
|
+
onSessionStart: ExtensionHandler<SessionStartEvent>;
|
|
32
|
+
onSessionTree: ExtensionHandler<SessionTreeEvent>;
|
|
33
|
+
onBeforeAgentStart: ExtensionHandler<BeforeAgentStartEvent, undefined>;
|
|
34
|
+
onMessageStart: ExtensionHandler<MessageStartEvent>;
|
|
35
|
+
onTurnStart: ExtensionHandler<TurnStartEvent>;
|
|
36
|
+
onToolExecutionEnd: ExtensionHandler<ToolExecutionEndEvent>;
|
|
37
|
+
onTurnEnd: ExtensionHandler<TurnEndEvent>;
|
|
38
|
+
onAgentEnd: ExtensionHandler<AgentEndEvent>;
|
|
39
|
+
onSessionBeforeCompact: ExtensionHandler<SessionBeforeCompactEvent>;
|
|
40
|
+
onSessionCompact: ExtensionHandler<SessionCompactEvent>;
|
|
41
|
+
onSessionShutdown: ExtensionHandler<SessionShutdownEvent>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GoalRuntimeStatusPort {
|
|
45
|
+
refreshUi: (ctx: ExtensionContext) => void;
|
|
46
|
+
stopStatusRefresh: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GoalRuntimeContinuationPort {
|
|
50
|
+
bindPassthroughContinuationInputToTurn: (turnIndex: number) => void;
|
|
51
|
+
clearContinuationState: () => void;
|
|
52
|
+
clearContinuationStateFor: (goalId: string) => void;
|
|
53
|
+
clearContinuationTimer: () => void;
|
|
54
|
+
clearPassthroughContinuationInput: () => void;
|
|
55
|
+
continuationGoalIdFromRuntimePrompt: (prompt: string) => string | null;
|
|
56
|
+
notePassthroughContinuationInput: (input: string) => void;
|
|
57
|
+
requestContinuation: (ctx: ExtensionContext) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GoalAccountingPort {
|
|
61
|
+
accountProgress: (
|
|
62
|
+
ctx: ExtensionContext,
|
|
63
|
+
includeActiveElapsed: boolean,
|
|
64
|
+
completedTurnTokens: number,
|
|
65
|
+
forceFlush?: boolean,
|
|
66
|
+
) => void;
|
|
67
|
+
beginAccounting: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RecoveryRuntimePort {
|
|
71
|
+
finishSuccessfulAssistantTurn: (
|
|
72
|
+
message: TurnEndEvent["message"],
|
|
73
|
+
ctx: ExtensionContext,
|
|
74
|
+
options: { continueGoal: boolean },
|
|
75
|
+
) => void;
|
|
76
|
+
handlePersistentAssistantError: (message: AssistantErrorMessage, ctx: ExtensionContext) => void;
|
|
77
|
+
handleSilentContextOverflow: (ctx: ExtensionContext) => void;
|
|
78
|
+
onSessionCompact: () => void;
|
|
79
|
+
onUserInput: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StaleQueuedWorkEffectContext {
|
|
83
|
+
status: GoalRuntimeStatusPort;
|
|
84
|
+
clearActiveAccounting: () => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GoalRuntimeInputContextHandlerContext extends StaleQueuedWorkEffectContext {
|
|
88
|
+
runtimeState: Pick<GoalRuntimeState, "currentTurnIndex" | "staleQueuedWorkGuard">;
|
|
89
|
+
stateController: Pick<
|
|
90
|
+
GoalStateController,
|
|
91
|
+
"getGoal" | "isCurrentActiveGoalId" | "persistHostOverflowUserReset"
|
|
92
|
+
>;
|
|
93
|
+
continuation: GoalRuntimeContinuationPort;
|
|
94
|
+
recoveryRuntime: Pick<RecoveryRuntimePort, "onUserInput">;
|
|
95
|
+
resetErrorRecovery: () => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface GoalRuntimeTurnHandlerContext extends StaleQueuedWorkEffectContext {
|
|
99
|
+
runtimeState: Pick<GoalRuntimeState, "currentTurnIndex" | "staleQueuedWorkGuard">;
|
|
100
|
+
stateController: Pick<
|
|
101
|
+
GoalStateController,
|
|
102
|
+
"beginOverflowRecovery" | "flushGoalPersistence" | "maybeFlushRuntimePersistence" | "pauseForAbort"
|
|
103
|
+
>;
|
|
104
|
+
continuation: Pick<GoalRuntimeContinuationPort, "bindPassthroughContinuationInputToTurn">;
|
|
105
|
+
goalAccounting: GoalAccountingPort;
|
|
106
|
+
recoveryRuntime: Pick<RecoveryRuntimePort, "finishSuccessfulAssistantTurn">;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface GoalRuntimeAgentHandlerContext extends StaleQueuedWorkEffectContext {
|
|
110
|
+
runtimeState: Pick<GoalRuntimeState, "staleQueuedWorkGuard">;
|
|
111
|
+
stateController: Pick<GoalStateController, "beginOverflowRecovery" | "flushGoalPersistence" | "pauseForAbort">;
|
|
112
|
+
continuation: Pick<
|
|
113
|
+
GoalRuntimeContinuationPort,
|
|
114
|
+
"clearPassthroughContinuationInput" | "requestContinuation"
|
|
115
|
+
>;
|
|
116
|
+
goalAccounting: Pick<GoalAccountingPort, "accountProgress">;
|
|
117
|
+
recoveryRuntime: Pick<
|
|
118
|
+
RecoveryRuntimePort,
|
|
119
|
+
"handlePersistentAssistantError" | "handleSilentContextOverflow"
|
|
120
|
+
>;
|
|
121
|
+
resetErrorRecovery: () => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface GoalRuntimeSessionHandlerContext extends StaleQueuedWorkEffectContext {
|
|
125
|
+
runtimeState: Pick<GoalRuntimeState, "recoveryState" | "staleQueuedWorkGuard">;
|
|
126
|
+
stateController: Pick<
|
|
127
|
+
GoalStateController,
|
|
128
|
+
"applyGoalTransition" | "flushGoalPersistence" | "getGoal" | "reloadFromSession" | "resumePausedGoal"
|
|
129
|
+
>;
|
|
130
|
+
continuation: Pick<
|
|
131
|
+
GoalRuntimeContinuationPort,
|
|
132
|
+
"clearContinuationTimer" | "clearPassthroughContinuationInput" | "requestContinuation"
|
|
133
|
+
>;
|
|
134
|
+
goalAccounting: GoalAccountingPort;
|
|
135
|
+
recoveryRuntime: Pick<RecoveryRuntimePort, "onSessionCompact">;
|
|
136
|
+
resetErrorRecovery: () => void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface GoalRuntimeOverflowRecoveryContext {
|
|
140
|
+
stateController: Pick<GoalStateController, "beginOverflowRecovery">;
|
|
141
|
+
recoveryRuntime: Pick<
|
|
142
|
+
RecoveryRuntimePort,
|
|
143
|
+
"handlePersistentAssistantError" | "handleSilentContextOverflow"
|
|
144
|
+
>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface GoalRuntimeEventContext {
|
|
148
|
+
pi: ExtensionAPI;
|
|
149
|
+
runtimeState: GoalRuntimeState;
|
|
150
|
+
stateController: GoalStateController;
|
|
151
|
+
continuation: GoalRuntimeContinuationPort;
|
|
152
|
+
goalAccounting: GoalAccountingPort;
|
|
153
|
+
recoveryRuntime: RecoveryRuntimePort;
|
|
154
|
+
status: GoalRuntimeStatusPort;
|
|
155
|
+
clearActiveAccounting: () => void;
|
|
156
|
+
resetErrorRecovery: () => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type QueuedGoalWorkMessage = {
|
|
160
|
+
role: string;
|
|
161
|
+
customType?: string;
|
|
162
|
+
details?: unknown;
|
|
163
|
+
content?: unknown;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type QueuedGoalWorkMessageIdResolver = (message: QueuedGoalWorkMessage) => string | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createAgentEventHandlers } from "./goal-runtime-agent-handlers.js";
|
|
2
|
+
import { createInputContextEventHandlers } from "./goal-runtime-input-context-handlers.js";
|
|
3
|
+
import { createSessionEventHandlers } from "./goal-runtime-session-handlers.js";
|
|
4
|
+
import { createTurnEventHandlers } from "./goal-runtime-turn-handlers.js";
|
|
5
|
+
import { createQueuedGoalWorkMessageIdResolver } from "./goal-runtime-event-utils.js";
|
|
6
|
+
import type {
|
|
7
|
+
GoalRuntimeEventContext,
|
|
8
|
+
GoalRuntimeEventHandlers,
|
|
9
|
+
} from "./goal-runtime-event-handler-types.js";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
ContextEventResult,
|
|
13
|
+
GoalRuntimeEventHandlers,
|
|
14
|
+
MessageStartEvent,
|
|
15
|
+
ToolExecutionEndEvent,
|
|
16
|
+
} from "./goal-runtime-event-handler-types.js";
|
|
17
|
+
|
|
18
|
+
export function createGoalRuntimeEventHandlers(
|
|
19
|
+
context: GoalRuntimeEventContext,
|
|
20
|
+
): GoalRuntimeEventHandlers {
|
|
21
|
+
const queuedGoalWorkMessageIdForRuntime = createQueuedGoalWorkMessageIdResolver(
|
|
22
|
+
context.continuation,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...createInputContextEventHandlers(context, queuedGoalWorkMessageIdForRuntime),
|
|
27
|
+
...createTurnEventHandlers(context),
|
|
28
|
+
...createAgentEventHandlers(context),
|
|
29
|
+
...createSessionEventHandlers(context),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
GoalRuntimeContinuationPort,
|
|
5
|
+
GoalRuntimeOverflowRecoveryContext,
|
|
6
|
+
QueuedGoalWorkMessage,
|
|
7
|
+
QueuedGoalWorkMessageIdResolver,
|
|
8
|
+
StaleQueuedWorkEffectContext,
|
|
9
|
+
} from "./goal-runtime-event-handler-types.js";
|
|
10
|
+
import { extensionQueuedGoalWorkMessageIdForRuntime } from "./queued-goal-work.js";
|
|
11
|
+
import {
|
|
12
|
+
isAssistantContextOverflow,
|
|
13
|
+
isContextOverflowError,
|
|
14
|
+
isErrorAssistantMessage,
|
|
15
|
+
type AssistantErrorMessage,
|
|
16
|
+
} from "./recovery.js";
|
|
17
|
+
import type { StaleQueuedWorkEffect, StaleQueuedWorkPlan } from "./stale-queued-work-guard.js";
|
|
18
|
+
|
|
19
|
+
export function applyStaleQueuedWorkEffects(
|
|
20
|
+
effects: readonly StaleQueuedWorkEffect[],
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
context: StaleQueuedWorkEffectContext,
|
|
23
|
+
): void {
|
|
24
|
+
for (const effect of effects) {
|
|
25
|
+
switch (effect.type) {
|
|
26
|
+
case "clearAccounting":
|
|
27
|
+
context.clearActiveAccounting();
|
|
28
|
+
break;
|
|
29
|
+
case "refreshUi":
|
|
30
|
+
context.status.refreshUi(ctx);
|
|
31
|
+
break;
|
|
32
|
+
case "abort":
|
|
33
|
+
ctx.abort();
|
|
34
|
+
break;
|
|
35
|
+
default: {
|
|
36
|
+
const _exhaustive: never = effect;
|
|
37
|
+
throw new Error(`Unhandled stale queued-work effect: ${String(_exhaustive)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runStaleQueuedWorkPlan(
|
|
44
|
+
plan: StaleQueuedWorkPlan,
|
|
45
|
+
ctx: ExtensionContext,
|
|
46
|
+
context: StaleQueuedWorkEffectContext,
|
|
47
|
+
): boolean {
|
|
48
|
+
applyStaleQueuedWorkEffects(plan.effects, ctx, context);
|
|
49
|
+
return plan.skip;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createQueuedGoalWorkMessageIdResolver(
|
|
53
|
+
continuation: GoalRuntimeContinuationPort,
|
|
54
|
+
): QueuedGoalWorkMessageIdResolver {
|
|
55
|
+
return (message: QueuedGoalWorkMessage): string | null =>
|
|
56
|
+
extensionQueuedGoalWorkMessageIdForRuntime(
|
|
57
|
+
message,
|
|
58
|
+
continuation.continuationGoalIdFromRuntimePrompt,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getContextWindow(ctx: ExtensionContext): number {
|
|
63
|
+
return ctx.model?.contextWindow ?? 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function recordAssistantContextOverflow(
|
|
67
|
+
message: AssistantErrorMessage,
|
|
68
|
+
ctx: ExtensionContext,
|
|
69
|
+
context: GoalRuntimeOverflowRecoveryContext,
|
|
70
|
+
): boolean {
|
|
71
|
+
if (!isAssistantContextOverflow(message, getContextWindow(ctx))) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
context.stateController.beginOverflowRecovery(ctx);
|
|
76
|
+
if (isErrorAssistantMessage(message)) {
|
|
77
|
+
context.recoveryRuntime.handlePersistentAssistantError(message, ctx);
|
|
78
|
+
} else {
|
|
79
|
+
context.recoveryRuntime.handleSilentContextOverflow(ctx);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function handleAgentErrorMessage(
|
|
85
|
+
message: AssistantErrorMessage,
|
|
86
|
+
ctx: ExtensionContext,
|
|
87
|
+
context: GoalRuntimeOverflowRecoveryContext,
|
|
88
|
+
): void {
|
|
89
|
+
recordAssistantContextOverflow(message, ctx, context);
|
|
90
|
+
if (!isContextOverflowError(message.errorMessage)) {
|
|
91
|
+
context.recoveryRuntime.handlePersistentAssistantError(message, ctx);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { GoalRuntimeController } from "./goal-runtime-controller.js";
|
|
4
|
+
|
|
5
|
+
export function registerGoalRuntimeEvents(
|
|
6
|
+
pi: ExtensionAPI,
|
|
7
|
+
controller: GoalRuntimeController,
|
|
8
|
+
): void {
|
|
9
|
+
pi.on("input", (event, ctx) => controller.onInput(event, ctx));
|
|
10
|
+
pi.on("context", (event, ctx) => controller.onContext(event, ctx));
|
|
11
|
+
pi.on("session_start", (event, ctx) => controller.onSessionStart(event, ctx));
|
|
12
|
+
pi.on("session_tree", (event, ctx) => controller.onSessionTree(event, ctx));
|
|
13
|
+
pi.on("before_agent_start", (event, ctx) => controller.onBeforeAgentStart(event, ctx));
|
|
14
|
+
pi.on("message_start", (event, ctx) => controller.onMessageStart(event, ctx));
|
|
15
|
+
pi.on("turn_start", (event, ctx) => controller.onTurnStart(event, ctx));
|
|
16
|
+
pi.on("tool_execution_end", (event, ctx) => controller.onToolExecutionEnd(event, ctx));
|
|
17
|
+
pi.on("turn_end", (event, ctx) => controller.onTurnEnd(event, ctx));
|
|
18
|
+
pi.on("agent_end", (event, ctx) => controller.onAgentEnd(event, ctx));
|
|
19
|
+
pi.on("session_before_compact", (event, ctx) =>
|
|
20
|
+
controller.onSessionBeforeCompact(event, ctx),
|
|
21
|
+
);
|
|
22
|
+
pi.on("session_compact", (event, ctx) => controller.onSessionCompact(event, ctx));
|
|
23
|
+
pi.on("session_shutdown", (event, ctx) => controller.onSessionShutdown(event, ctx));
|
|
24
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BeforeAgentStartEvent,
|
|
3
|
+
ContextEvent,
|
|
4
|
+
ExtensionHandler,
|
|
5
|
+
InputEvent,
|
|
6
|
+
InputEventResult,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
import { continuationGoalIdFromPrompt } from "./prompts.js";
|
|
10
|
+
import { applyQueuedGoalProviderContextRewrites, extensionQueuedGoalWorkMessageId } from "./queued-goal-work.js";
|
|
11
|
+
import { isActiveGoalQueuedDetails, isCommandResumeQueuedGoalMessage } from "./queued-goal-messages.js";
|
|
12
|
+
import { applyStaleQueuedWorkEffects } from "./goal-runtime-event-utils.js";
|
|
13
|
+
import type {
|
|
14
|
+
ContextEventResult,
|
|
15
|
+
GoalRuntimeInputContextHandlerContext,
|
|
16
|
+
MessageStartEvent,
|
|
17
|
+
QueuedGoalWorkMessageIdResolver,
|
|
18
|
+
} from "./goal-runtime-event-handler-types.js";
|
|
19
|
+
|
|
20
|
+
function goalIdFromQueuedDetails(details: unknown): string | null {
|
|
21
|
+
return isActiveGoalQueuedDetails(details) ? details.goalId : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function goalIdFromEventDetails(event: unknown): string | null {
|
|
25
|
+
if (!event || typeof event !== "object") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const candidate = event as { details?: unknown; message?: { details?: unknown } };
|
|
29
|
+
return goalIdFromQueuedDetails(candidate.details) ?? goalIdFromQueuedDetails(candidate.message?.details);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createInputContextEventHandlers(
|
|
33
|
+
deps: GoalRuntimeInputContextHandlerContext,
|
|
34
|
+
queuedGoalWorkMessageIdForRuntime: QueuedGoalWorkMessageIdResolver,
|
|
35
|
+
) {
|
|
36
|
+
const { runtimeState, stateController, continuation, recoveryRuntime, status, resetErrorRecovery } = deps;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
onInput: (async (event, ctx) => {
|
|
40
|
+
continuation.clearPassthroughContinuationInput();
|
|
41
|
+
const continuationGoalId = goalIdFromEventDetails(event) ?? continuationGoalIdFromPrompt(event.text);
|
|
42
|
+
|
|
43
|
+
if (event.source !== "extension") {
|
|
44
|
+
recoveryRuntime.onUserInput();
|
|
45
|
+
applyStaleQueuedWorkEffects(
|
|
46
|
+
runtimeState.staleQueuedWorkGuard.planUserInputClearAbort().effects,
|
|
47
|
+
ctx,
|
|
48
|
+
deps,
|
|
49
|
+
);
|
|
50
|
+
if (continuationGoalId !== null) {
|
|
51
|
+
continuation.notePassthroughContinuationInput(event.text);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (continuationGoalId === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
applyStaleQueuedWorkEffects(
|
|
61
|
+
runtimeState.staleQueuedWorkGuard.planExtensionContinuationClearAbort().effects,
|
|
62
|
+
ctx,
|
|
63
|
+
deps,
|
|
64
|
+
);
|
|
65
|
+
continuation.clearContinuationStateFor(continuationGoalId);
|
|
66
|
+
if (stateController.isCurrentActiveGoalId(continuationGoalId)) {
|
|
67
|
+
return { action: "continue" } as const;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
status.refreshUi(ctx);
|
|
71
|
+
return { action: "handled" } as const;
|
|
72
|
+
}) satisfies ExtensionHandler<InputEvent, InputEventResult>,
|
|
73
|
+
|
|
74
|
+
onContext: (async (event, ctx) => {
|
|
75
|
+
const { messages, changed } = applyQueuedGoalProviderContextRewrites(event.messages, {
|
|
76
|
+
goal: stateController.getGoal(),
|
|
77
|
+
resolveStaleQueuedGoalWorkMessageId: queuedGoalWorkMessageIdForRuntime,
|
|
78
|
+
resolveActiveContinuationQueuedGoalWorkMessageId: extensionQueuedGoalWorkMessageId,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const contextAbortPlan = runtimeState.staleQueuedWorkGuard.planContextAbort(
|
|
82
|
+
runtimeState.currentTurnIndex,
|
|
83
|
+
);
|
|
84
|
+
if (contextAbortPlan !== null) {
|
|
85
|
+
applyStaleQueuedWorkEffects(contextAbortPlan.effects, ctx, deps);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return changed ? { messages } : undefined;
|
|
89
|
+
}) satisfies ExtensionHandler<ContextEvent, ContextEventResult | undefined>,
|
|
90
|
+
|
|
91
|
+
onBeforeAgentStart: (async (event, ctx) => {
|
|
92
|
+
const continuationGoalId =
|
|
93
|
+
goalIdFromEventDetails(event) ?? continuation.continuationGoalIdFromRuntimePrompt(event.prompt);
|
|
94
|
+
if (continuationGoalId !== null) {
|
|
95
|
+
continuation.clearContinuationStateFor(continuationGoalId);
|
|
96
|
+
if (!stateController.isCurrentActiveGoalId(continuationGoalId)) {
|
|
97
|
+
runtimeState.staleQueuedWorkGuard.noteStaleWorkStarted(continuationGoalId);
|
|
98
|
+
status.refreshUi(ctx);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
applyStaleQueuedWorkEffects(
|
|
102
|
+
runtimeState.staleQueuedWorkGuard.planBeforeAgentStartClearAbort().effects,
|
|
103
|
+
ctx,
|
|
104
|
+
deps,
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
applyStaleQueuedWorkEffects(
|
|
108
|
+
runtimeState.staleQueuedWorkGuard.planBeforeAgentStartClearAbort().effects,
|
|
109
|
+
ctx,
|
|
110
|
+
deps,
|
|
111
|
+
);
|
|
112
|
+
continuation.clearContinuationState();
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}) satisfies ExtensionHandler<BeforeAgentStartEvent, undefined>,
|
|
116
|
+
|
|
117
|
+
onMessageStart: (async (event, _ctx) => {
|
|
118
|
+
if (event.message.role === "user") {
|
|
119
|
+
stateController.persistHostOverflowUserReset(false);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const queuedGoalId = queuedGoalWorkMessageIdForRuntime(event.message);
|
|
123
|
+
if (queuedGoalId === null) {
|
|
124
|
+
if (event.message.role === "user" || event.message.role === "custom") {
|
|
125
|
+
runtimeState.staleQueuedWorkGuard.noteRunnableWorkStarted();
|
|
126
|
+
continuation.clearContinuationState();
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
continuation.clearContinuationStateFor(queuedGoalId);
|
|
132
|
+
if (stateController.isCurrentActiveGoalId(queuedGoalId)) {
|
|
133
|
+
stateController.persistHostOverflowUserReset(false);
|
|
134
|
+
runtimeState.staleQueuedWorkGuard.noteRunnableWorkStarted();
|
|
135
|
+
if (isCommandResumeQueuedGoalMessage(event.message)) {
|
|
136
|
+
resetErrorRecovery();
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
runtimeState.staleQueuedWorkGuard.noteStaleWorkStarted(queuedGoalId);
|
|
142
|
+
}) satisfies ExtensionHandler<MessageStartEvent>,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
ExtensionHandler,
|
|
4
|
+
SessionBeforeCompactEvent,
|
|
5
|
+
SessionCompactEvent,
|
|
6
|
+
SessionShutdownEvent,
|
|
7
|
+
SessionStartEvent,
|
|
8
|
+
SessionTreeEvent,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
import { recoveryPhaseBlocksContinuation } from "./recovery-machine.js";
|
|
12
|
+
import { isRecoveryPendingAttention, reasonFromRecoveryPendingAttention } from "./recovery.js";
|
|
13
|
+
import { applyStaleQueuedWorkEffects, runStaleQueuedWorkPlan } from "./goal-runtime-event-utils.js";
|
|
14
|
+
import type { GoalRuntimeSessionHandlerContext } from "./goal-runtime-event-handler-types.js";
|
|
15
|
+
|
|
16
|
+
export function createSessionEventHandlers(deps: GoalRuntimeSessionHandlerContext) {
|
|
17
|
+
const {
|
|
18
|
+
runtimeState,
|
|
19
|
+
stateController,
|
|
20
|
+
continuation,
|
|
21
|
+
goalAccounting,
|
|
22
|
+
recoveryRuntime,
|
|
23
|
+
status,
|
|
24
|
+
resetErrorRecovery,
|
|
25
|
+
} = deps;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
onSessionStart: (async (event, ctx) => {
|
|
29
|
+
stateController.reloadFromSession(ctx);
|
|
30
|
+
goalAccounting.beginAccounting();
|
|
31
|
+
const goal = stateController.getGoal();
|
|
32
|
+
const pausedGoal = goal?.status === "paused" ? goal : null;
|
|
33
|
+
if (event.reason === "resume" && pausedGoal && ctx.hasUI) {
|
|
34
|
+
const shouldResume = await ctx.ui.confirm(
|
|
35
|
+
"Resume paused goal?",
|
|
36
|
+
`Goal: ${pausedGoal.objective}`,
|
|
37
|
+
);
|
|
38
|
+
if (shouldResume) {
|
|
39
|
+
stateController.resumePausedGoal(ctx);
|
|
40
|
+
goalAccounting.beginAccounting();
|
|
41
|
+
const resumedGoal = stateController.getGoal();
|
|
42
|
+
if (resumedGoal?.status === "active") {
|
|
43
|
+
continuation.requestContinuation(ctx);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
continuation.requestContinuation(ctx);
|
|
49
|
+
}) satisfies ExtensionHandler<SessionStartEvent>,
|
|
50
|
+
|
|
51
|
+
onSessionTree: (async (_event, ctx) => {
|
|
52
|
+
stateController.reloadFromSession(ctx);
|
|
53
|
+
goalAccounting.beginAccounting();
|
|
54
|
+
continuation.requestContinuation(ctx);
|
|
55
|
+
}) satisfies ExtensionHandler<SessionTreeEvent>,
|
|
56
|
+
|
|
57
|
+
onSessionBeforeCompact: (async (_event, ctx) => {
|
|
58
|
+
if (
|
|
59
|
+
runStaleQueuedWorkPlan(
|
|
60
|
+
runtimeState.staleQueuedWorkGuard.planSessionBeforeCompact(),
|
|
61
|
+
ctx,
|
|
62
|
+
deps,
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
goalAccounting.accountProgress(ctx, false, 0, true);
|
|
69
|
+
stateController.flushGoalPersistence("runtime");
|
|
70
|
+
}) satisfies ExtensionHandler<SessionBeforeCompactEvent>,
|
|
71
|
+
|
|
72
|
+
onSessionCompact: (async (_event, ctx) => {
|
|
73
|
+
if (runStaleQueuedWorkPlan(runtimeState.staleQueuedWorkGuard.planSessionCompact(), ctx, deps)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stateController.flushGoalPersistence("runtime");
|
|
78
|
+
recoveryRuntime.onSessionCompact();
|
|
79
|
+
status.refreshUi(ctx);
|
|
80
|
+
if (!recoveryPhaseBlocksContinuation(runtimeState.recoveryState.phase)) {
|
|
81
|
+
continuation.requestContinuation(ctx);
|
|
82
|
+
}
|
|
83
|
+
}) satisfies ExtensionHandler<SessionCompactEvent>,
|
|
84
|
+
|
|
85
|
+
onSessionShutdown: (async (_event, ctx) => {
|
|
86
|
+
continuation.clearPassthroughContinuationInput();
|
|
87
|
+
applyStaleQueuedWorkEffects(runtimeState.staleQueuedWorkGuard.planSessionShutdown().effects, ctx, deps);
|
|
88
|
+
|
|
89
|
+
goalAccounting.accountProgress(ctx, false, 0, true);
|
|
90
|
+
stateController.flushGoalPersistence("runtime");
|
|
91
|
+
continuation.clearContinuationTimer();
|
|
92
|
+
if (hasPendingRecoveryAttention(deps)) {
|
|
93
|
+
pauseForPendingRecoveryShutdown(ctx, deps);
|
|
94
|
+
} else {
|
|
95
|
+
resetErrorRecovery();
|
|
96
|
+
}
|
|
97
|
+
status.stopStatusRefresh();
|
|
98
|
+
}) satisfies ExtensionHandler<SessionShutdownEvent>,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasPendingRecoveryAttention({ runtimeState, stateController }: GoalRuntimeSessionHandlerContext): boolean {
|
|
103
|
+
const goal = stateController.getGoal();
|
|
104
|
+
return Boolean(
|
|
105
|
+
goal?.status === "active" && isRecoveryPendingAttention(runtimeState.recoveryState.attention),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pauseForPendingRecoveryShutdown(
|
|
110
|
+
ctx: ExtensionContext,
|
|
111
|
+
deps: GoalRuntimeSessionHandlerContext,
|
|
112
|
+
): void {
|
|
113
|
+
const { runtimeState, stateController } = deps;
|
|
114
|
+
const goal = stateController.getGoal();
|
|
115
|
+
if (!goal || goal.status !== "active" || !runtimeState.recoveryState.attention) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const reason = reasonFromRecoveryPendingAttention(runtimeState.recoveryState.attention);
|
|
120
|
+
if (!reason) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stateController.applyGoalTransition(
|
|
125
|
+
{
|
|
126
|
+
kind: "recovery_shutdown_pause",
|
|
127
|
+
recoveryReason: reason,
|
|
128
|
+
},
|
|
129
|
+
ctx,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createAccountingState, type AccountingState } from "./goal-accounting.js";
|
|
2
|
+
import { createGoalRecoveryMachine, type GoalRecoveryMachineState } from "./recovery-machine.js";
|
|
3
|
+
import {
|
|
4
|
+
createStaleQueuedWorkGuard,
|
|
5
|
+
type StaleQueuedWorkGuard,
|
|
6
|
+
} from "./stale-queued-work-guard.js";
|
|
7
|
+
|
|
8
|
+
export interface GoalRuntimeState {
|
|
9
|
+
accounting: AccountingState;
|
|
10
|
+
recoveryState: GoalRecoveryMachineState;
|
|
11
|
+
currentTurnIndex: number | null;
|
|
12
|
+
staleQueuedWorkGuard: StaleQueuedWorkGuard;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createGoalRuntimeState(): GoalRuntimeState {
|
|
16
|
+
return {
|
|
17
|
+
accounting: createAccountingState(),
|
|
18
|
+
recoveryState: createGoalRecoveryMachine(),
|
|
19
|
+
currentTurnIndex: null,
|
|
20
|
+
staleQueuedWorkGuard: createStaleQueuedWorkGuard(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { formatFooterStatus } from "./format.js";
|
|
4
|
+
import type { GoalRecoveryMachineState } from "./recovery-machine.js";
|
|
5
|
+
import type { ThreadGoal } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface StatusContext {
|
|
8
|
+
ui: Pick<ExtensionContext["ui"], "setStatus">;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface GoalRuntimeStatusDeps {
|
|
12
|
+
getGoalForDisplay: () => ThreadGoal | null;
|
|
13
|
+
getGoalStatus: () => ThreadGoal["status"] | null;
|
|
14
|
+
getRecoveryAttention: () => GoalRecoveryMachineState["attention"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createGoalRuntimeStatus(deps: GoalRuntimeStatusDeps) {
|
|
18
|
+
let statusContext: StatusContext | null = null;
|
|
19
|
+
let statusRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
|
|
21
|
+
const stopStatusRefresh = (): void => {
|
|
22
|
+
if (statusRefreshTimer) {
|
|
23
|
+
clearInterval(statusRefreshTimer);
|
|
24
|
+
statusRefreshTimer = null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const syncStatusRefresh = (): void => {
|
|
29
|
+
if (deps.getGoalStatus() === "active" && statusContext && !statusRefreshTimer) {
|
|
30
|
+
statusRefreshTimer = setInterval(() => {
|
|
31
|
+
if (!statusContext || deps.getGoalStatus() !== "active") {
|
|
32
|
+
stopStatusRefresh();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
statusContext.ui.setStatus(
|
|
36
|
+
"codex-goal",
|
|
37
|
+
formatFooterStatus(deps.getGoalForDisplay(), deps.getRecoveryAttention()),
|
|
38
|
+
);
|
|
39
|
+
}, 1_000);
|
|
40
|
+
statusRefreshTimer.unref?.();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (deps.getGoalStatus() !== "active") {
|
|
45
|
+
stopStatusRefresh();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const refreshUi = (ctx: StatusContext): void => {
|
|
50
|
+
statusContext = ctx;
|
|
51
|
+
ctx.ui.setStatus(
|
|
52
|
+
"codex-goal",
|
|
53
|
+
formatFooterStatus(deps.getGoalForDisplay(), deps.getRecoveryAttention()),
|
|
54
|
+
);
|
|
55
|
+
syncStatusRefresh();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
refreshUi,
|
|
60
|
+
stopStatusRefresh,
|
|
61
|
+
};
|
|
62
|
+
}
|