Files
Agent-System/web/src/lib/squadCompletionObservation.ts
T
weichenandCursor ab9fccded4 feat(web): disable unmanaged approval actions
Hide submit/review/approve on none docs, surface the blocked-run hint in
chat, and warn that unmanaged duties cannot be added after activation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 08:00:21 +00:00

103 lines
3.5 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',
'blocked_pending_governance',
] as const;
export const SUCCESS_TERMINAL_STATUS_RE =
/^(completed|pending_author_confirmation)$/;
export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed|blocked_pending_governance)$/;
export const ANY_TERMINAL_STATUS_RE =
/^(completed|pending_author_confirmation|failed|cancelled|quality_failed|blocked_pending_governance)$/;
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;
}