Files
makelore/src/stores/opencode-session-run-machine.ts

219 lines
5.5 KiB
TypeScript

export type SessionRunQueuedPrompt = {
id: string;
text: string;
};
export type SessionRunPhase = 'idle' | 'posting' | 'running' | 'aborting';
export type SessionRunTerminalReason = 'completed' | 'aborted' | 'failed';
export type SessionRunState<TPrompt extends SessionRunQueuedPrompt = SessionRunQueuedPrompt> = {
phase: SessionRunPhase;
runId: number | null;
promptId: string | null;
queue: TPrompt[];
terminalReason: SessionRunTerminalReason | null;
error: string | null;
suppressNextAbortError: boolean;
};
export type SessionRunEvent<TPrompt extends SessionRunQueuedPrompt = SessionRunQueuedPrompt> =
| { type: 'send_started'; runId: number; promptId: string }
| { type: 'post_accepted'; runId: number }
| { type: 'remote_busy'; runId: number }
| { type: 'remote_idle'; runId: number }
| { type: 'remote_aborted'; runId: number }
| { type: 'remote_failed'; runId: number; error: string }
| { type: 'abort_requested'; runId: number }
| { type: 'abort_confirmed'; runId: number }
| { type: 'abort_failed'; runId: number; error: string }
| { type: 'queue_prompt'; prompt: TPrompt }
| { type: 'cancel_queued_prompt'; promptId: string };
export function createIdleSessionRunState<TPrompt extends SessionRunQueuedPrompt>(
queue: TPrompt[] = [],
): SessionRunState<TPrompt> {
return {
phase: 'idle',
runId: null,
promptId: null,
queue,
terminalReason: null,
error: null,
suppressNextAbortError: false,
};
}
export function isCurrentRunEvent<TPrompt extends SessionRunQueuedPrompt>(
state: SessionRunState<TPrompt>,
runId: number,
): boolean {
return state.runId === runId;
}
export function isSessionRunActive<TPrompt extends SessionRunQueuedPrompt>(
state: SessionRunState<TPrompt>,
): boolean {
return state.phase === 'posting' || state.phase === 'running';
}
export function canStartQueuedPrompt<TPrompt extends SessionRunQueuedPrompt>(
state: SessionRunState<TPrompt>,
): boolean {
return state.phase === 'idle'
&& state.terminalReason !== 'failed'
&& state.queue.length > 0;
}
export function takeNextQueuedPrompt<TPrompt extends SessionRunQueuedPrompt>(
state: SessionRunState<TPrompt>,
): {
prompt: TPrompt | null;
state: SessionRunState<TPrompt>;
} {
const [prompt, ...queue] = state.queue;
return {
prompt: prompt ?? null,
state: {
...state,
queue,
},
};
}
export function didSuppressAbortError<TPrompt extends SessionRunQueuedPrompt>(
previousState: SessionRunState<TPrompt>,
nextState: SessionRunState<TPrompt>,
): boolean {
return previousState.suppressNextAbortError
&& !nextState.suppressNextAbortError
&& previousState.phase === nextState.phase
&& previousState.runId === nextState.runId
&& previousState.promptId === nextState.promptId
&& nextState.terminalReason === previousState.terminalReason
&& nextState.error === null;
}
export function transitionSessionRunState<TPrompt extends SessionRunQueuedPrompt>(
state: SessionRunState<TPrompt>,
event: SessionRunEvent<TPrompt>,
): SessionRunState<TPrompt> {
if (event.type === 'send_started') {
return {
phase: 'posting',
runId: event.runId,
promptId: event.promptId,
queue: state.queue,
terminalReason: null,
error: null,
suppressNextAbortError: state.suppressNextAbortError,
};
}
if (event.type === 'queue_prompt') {
return {
...state,
queue: [...state.queue, event.prompt],
error: null,
};
}
if (event.type === 'cancel_queued_prompt') {
return {
...state,
queue: state.queue.filter((prompt) => prompt.id !== event.promptId),
};
}
if (!isCurrentRunEvent(state, event.runId)) {
return state;
}
if (
state.phase === 'idle'
&& state.terminalReason !== null
&& (
event.type === 'post_accepted'
|| event.type === 'remote_busy'
|| event.type === 'remote_idle'
)
) {
return state;
}
switch (event.type) {
case 'post_accepted':
return state;
case 'remote_busy':
return {
...state,
phase: 'running',
terminalReason: null,
error: null,
};
case 'remote_idle':
if (state.phase === 'posting') return state;
return {
...state,
phase: 'idle',
terminalReason: 'completed',
error: null,
suppressNextAbortError: false,
};
case 'remote_aborted':
if (state.suppressNextAbortError) {
return {
...state,
error: null,
suppressNextAbortError: false,
};
}
return {
...state,
phase: 'idle',
queue: [],
terminalReason: 'aborted',
error: null,
suppressNextAbortError: false,
};
case 'remote_failed':
return {
...state,
phase: 'idle',
queue: [],
terminalReason: 'failed',
error: event.error,
suppressNextAbortError: false,
};
case 'abort_requested':
return {
...state,
phase: 'aborting',
queue: [],
terminalReason: 'aborted',
error: null,
suppressNextAbortError: true,
};
case 'abort_confirmed':
return {
...state,
phase: 'idle',
queue: [],
terminalReason: 'aborted',
error: null,
suppressNextAbortError: false,
};
case 'abort_failed':
return {
...state,
phase: 'idle',
queue: [],
terminalReason: 'failed',
error: event.error,
suppressNextAbortError: false,
};
default:
return state;
}
}