UI-11 can finish before the waiter sees pending. Treat success terminals as done, keep upload/correspondence/error checks, and cover both timelines. Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
export const INFLIGHT_CHAT_STATUSES = ['pending', 'streaming_status', 'auto_retrying'] as const;
|
|
export const SUCCESS_TERMINAL_CHAT_STATUSES = ['completed', 'pending_author_confirmation'] as const;
|
|
export const ERROR_TERMINAL_CHAT_STATUSES = ['failed', 'cancelled', 'quality_failed'] as const;
|
|
|
|
export const SUCCESS_TERMINAL_STATUS_RE =
|
|
/^(completed|pending_author_confirmation)$/;
|
|
export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed)$/;
|
|
export const ANY_TERMINAL_STATUS_RE =
|
|
/^(completed|pending_author_confirmation|failed|cancelled|quality_failed)$/;
|
|
|
|
export type SquadObservationFrame = {
|
|
userPromptMatched: boolean;
|
|
assistantAdjacent: boolean;
|
|
status: string;
|
|
hasVisibleError?: boolean;
|
|
};
|
|
|
|
export type SquadObservationVerdict = {
|
|
action: 'accept' | 'wait' | 'reject';
|
|
reason: string;
|
|
sawInflight: boolean;
|
|
};
|
|
|
|
export function isInflightChatStatus(status: string): boolean {
|
|
return (INFLIGHT_CHAT_STATUSES as readonly string[]).includes(status);
|
|
}
|
|
|
|
export function isSuccessTerminalChatStatus(status: string): boolean {
|
|
return (SUCCESS_TERMINAL_CHAT_STATUSES as readonly string[]).includes(status);
|
|
}
|
|
|
|
export function isErrorTerminalChatStatus(status: string): boolean {
|
|
return (ERROR_TERMINAL_CHAT_STATUSES as readonly string[]).includes(status);
|
|
}
|
|
|
|
/**
|
|
* Decide whether a single observed assistant card is done, still running, or invalid.
|
|
* Inflight is optional: a success terminal is accepted even if requireInflight is true
|
|
* and no pending/streaming frame was seen (UI-11 fast-complete).
|
|
*/
|
|
export function evaluateSquadObservation(
|
|
frame: SquadObservationFrame,
|
|
ctx?: { requireInflight?: boolean; sawInflight?: boolean },
|
|
): SquadObservationVerdict {
|
|
const sawInflight = Boolean(ctx?.sawInflight) || isInflightChatStatus(frame.status);
|
|
if (!frame.userPromptMatched) {
|
|
return { action: 'reject', reason: 'missing_user_prompt', sawInflight };
|
|
}
|
|
if (!frame.assistantAdjacent) {
|
|
return { action: 'reject', reason: 'missing_adjacent_assistant', sawInflight };
|
|
}
|
|
if (frame.hasVisibleError) {
|
|
return { action: 'reject', reason: 'visible_error', sawInflight };
|
|
}
|
|
if (isErrorTerminalChatStatus(frame.status)) {
|
|
return { action: 'reject', reason: `error_terminal:${frame.status}`, sawInflight };
|
|
}
|
|
if (isSuccessTerminalChatStatus(frame.status)) {
|
|
return {
|
|
action: 'accept',
|
|
reason: sawInflight && !isInflightChatStatus(frame.status)
|
|
? 'completed_after_inflight'
|
|
: 'already_completed',
|
|
sawInflight: Boolean(ctx?.sawInflight),
|
|
};
|
|
}
|
|
if (isInflightChatStatus(frame.status)) {
|
|
return { action: 'wait', reason: 'inflight', sawInflight: true };
|
|
}
|
|
return { action: 'wait', reason: 'wait_terminal', sawInflight: Boolean(ctx?.sawInflight) };
|
|
}
|
|
|
|
/** Replay a status timeline. Used by unit tests for direct-completed vs inflight paths. */
|
|
export function reduceSquadObservation(
|
|
frames: SquadObservationFrame[],
|
|
opts?: { requireInflight?: boolean },
|
|
): SquadObservationVerdict {
|
|
let sawInflight = false;
|
|
let last: SquadObservationVerdict = {
|
|
action: 'wait',
|
|
reason: 'empty_timeline',
|
|
sawInflight: false,
|
|
};
|
|
for (const frame of frames) {
|
|
if (isInflightChatStatus(frame.status)) {
|
|
sawInflight = true;
|
|
}
|
|
last = evaluateSquadObservation(frame, {
|
|
requireInflight: opts?.requireInflight,
|
|
sawInflight,
|
|
});
|
|
if (last.action !== 'wait') {
|
|
return last;
|
|
}
|
|
}
|
|
return last;
|
|
}
|