Plan actions post to the decision API and reconcile on 409 instead of mutating documents. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { getBearerAuthHeaders } from './authHeaders';
|
|
|
|
export type IntentPlanStatus =
|
|
| 'proposed'
|
|
| 'awaiting_clarification'
|
|
| 'awaiting_confirmation'
|
|
| 'executing'
|
|
| 'partially_completed'
|
|
| 'completed'
|
|
| 'failed'
|
|
| 'cancelled'
|
|
| 'expired'
|
|
| 'superseded';
|
|
|
|
export type IntentPlanView = {
|
|
plan_id: string;
|
|
revision: number;
|
|
status: IntentPlanStatus;
|
|
execution_mode?: string;
|
|
expires_at?: string;
|
|
safe_summary?: string;
|
|
steps?: Array<{
|
|
step_id: string;
|
|
action_key: string;
|
|
status: string;
|
|
execution_mode?: string;
|
|
depends_on?: string[];
|
|
safe_summary?: string;
|
|
}>;
|
|
};
|
|
|
|
export type IntentPlanEnvelope = {
|
|
plan: IntentPlanView | null;
|
|
error_code?: string;
|
|
message?: string;
|
|
};
|
|
|
|
function scopeQuery(projectId: number, sessionId: string): string {
|
|
return `project_id=${encodeURIComponent(String(projectId))}&session_id=${encodeURIComponent(sessionId)}`;
|
|
}
|
|
|
|
export async function fetchActiveIntentPlan(
|
|
projectId: number,
|
|
sessionId: string,
|
|
signal?: AbortSignal,
|
|
): Promise<IntentPlanView | null> {
|
|
const res = await fetch(`/api/v1/intent-plans/active?${scopeQuery(projectId, sessionId)}`, {
|
|
signal,
|
|
headers: getBearerAuthHeaders(),
|
|
});
|
|
if (res.status === 404) return null;
|
|
if (!res.ok) {
|
|
throw new Error(`fetchActiveIntentPlan failed: HTTP ${res.status}`);
|
|
}
|
|
const raw = (await res.json()) as IntentPlanEnvelope;
|
|
return raw.plan ?? null;
|
|
}
|
|
|
|
export async function fetchIntentPlan(
|
|
planId: string,
|
|
projectId: number,
|
|
sessionId: string,
|
|
signal?: AbortSignal,
|
|
): Promise<IntentPlanView | null> {
|
|
const res = await fetch(
|
|
`/api/v1/intent-plans/${encodeURIComponent(planId)}?${scopeQuery(projectId, sessionId)}`,
|
|
{ signal, headers: getBearerAuthHeaders() },
|
|
);
|
|
if (res.status === 404) return null;
|
|
if (!res.ok) {
|
|
throw new Error(`fetchIntentPlan failed: HTTP ${res.status}`);
|
|
}
|
|
const raw = (await res.json()) as IntentPlanEnvelope;
|
|
return raw.plan ?? null;
|
|
}
|
|
|
|
export type IntentPlanDecisionInput = {
|
|
planId: string;
|
|
projectId: number;
|
|
sessionId: string;
|
|
expectedRevision: number;
|
|
decisionNonce: string;
|
|
decisionDigest: string;
|
|
kind: 'confirm' | 'cancel' | 'exclude' | 'edit' | 'confirm_retry' | 'continue';
|
|
stepIds?: string[];
|
|
excludeUnselected?: boolean;
|
|
};
|
|
|
|
export async function submitIntentPlanDecision(
|
|
input: IntentPlanDecisionInput,
|
|
signal?: AbortSignal,
|
|
): Promise<IntentPlanEnvelope & { httpStatus: number }> {
|
|
const res = await fetch(
|
|
`/api/v1/intent-plans/${encodeURIComponent(input.planId)}/decisions?${scopeQuery(input.projectId, input.sessionId)}`,
|
|
{
|
|
method: 'POST',
|
|
signal,
|
|
headers: getBearerAuthHeaders('application/json'),
|
|
body: JSON.stringify({
|
|
expected_revision: input.expectedRevision,
|
|
decision_nonce: input.decisionNonce,
|
|
decision_digest: input.decisionDigest,
|
|
kind: input.kind,
|
|
step_ids: input.stepIds,
|
|
exclude_unselected: input.excludeUnselected,
|
|
}),
|
|
},
|
|
);
|
|
const raw = (await res.json().catch(() => ({}))) as IntentPlanEnvelope;
|
|
return { ...raw, httpStatus: res.status };
|
|
}
|