Files
ttstd-infinite-canvas/canvas-agent/src/dreamina-task-contract.ts

383 lines
14 KiB
TypeScript

export type DreaminaTaskLifecycle =
| "QUEUED_LOCAL"
| "SUBMITTING"
| "SUBMISSION_UNCERTAIN"
| "ACCEPTED"
| "RUNNING"
| "TERMINAL";
export type DreaminaTerminalOutcome =
| "SUCCEEDED"
| "REJECTED"
| "FAILED"
| "CANCELLED"
| "FAILED_OR_CANCELLED";
export type DreaminaTaskSyncState =
| "SYNC_OK"
| "SYNC_RETRY_WAIT"
| "SYNC_BLOCKED_ACCOUNT"
| "SYNC_UNCERTAIN"
| "SYNC_CONFLICT";
export type DreaminaTaskResultState =
| "NOT_AVAILABLE"
| "PENDING_MATERIALIZATION"
| "MATERIALIZING"
| "READY"
| "FAILED_RETRYABLE"
| "FAILED_PERMANENT";
export type DreaminaTaskMediaType = "image" | "video" | "audio";
export type DreaminaAvailability = "yes" | "no" | "unknown";
export type DreaminaTaskOutput = {
outputIndex: number;
mediaType: DreaminaTaskMediaType;
providerArtifactRef?: string;
materializedAssetId?: string;
materializationErrorCode?: string;
};
export type DreaminaScopedTaskContext = {
scope: "scoped";
projectId?: string;
nodeId?: string;
conversationId?: string;
messageId?: string;
batchIndex?: number;
batchCount?: number;
retryOf?: string;
attemptGroupId?: string;
};
export type DreaminaTaskContext = DreaminaScopedTaskContext | { scope: "legacy_unscoped" };
export type DreaminaProviderObservation = {
source: "submit_receipt" | "query_result" | "list_task";
observedAt: string;
accountBinding?: string;
fenceEpoch?: number;
status: "pending" | "processing" | "completed" | "failed" | "cancelled";
};
export type DreaminaProviderCapability = {
adapterSupported: boolean;
cancelSupported: boolean;
pushStatusSupported: boolean;
statusConsistency: "eventual_polling";
accountEntitlement: DreaminaAvailability;
currentlyObservedAvailable: DreaminaAvailability;
references: {
images: boolean;
videos: boolean;
audios: boolean;
firstLastFrames: boolean;
};
};
export type DreaminaGenerationTaskContract = {
taskId: string;
clientOperationId: string;
provider: "dreamina-cli";
accountBinding?: string;
lifecycle: DreaminaTaskLifecycle;
terminalOutcome?: DreaminaTerminalOutcome;
syncState: DreaminaTaskSyncState;
resultState: DreaminaTaskResultState;
projectId?: string;
nodeId?: string;
conversationId?: string;
messageId?: string;
batchIndex?: number;
batchCount?: number;
retryOf?: string;
attemptGroupId?: string;
requestHash: string;
providerTaskId?: string;
officialStatus?: DreaminaProviderObservation["status"];
lastObservedAt?: string;
lastSyncErrorCode?: string;
nextPollAt?: string;
version: number;
outputs: DreaminaTaskOutput[];
};
export type DreaminaTaskState = {
lifecycle: DreaminaTaskLifecycle;
terminalOutcome?: DreaminaTerminalOutcome;
syncState: DreaminaTaskSyncState;
resultState: DreaminaTaskResultState;
outputs: DreaminaTaskOutput[];
accountBinding?: string;
context: DreaminaTaskContext;
providerObservation?: DreaminaProviderObservation;
lastSyncErrorCode?: string;
};
export type DreaminaLocalSyncErrorKind = "query" | "transport" | "parse" | "lock" | "login" | "account";
export type DreaminaTaskEvent =
| { type: "transition"; lifecycle: DreaminaTaskLifecycle; terminalOutcome?: DreaminaTerminalOutcome }
| { type: "provider_observation"; observation: DreaminaProviderObservation }
| { type: "sync_error"; errorKind: DreaminaLocalSyncErrorKind; code: string }
| { type: "sync_recovered" }
| { type: "bind_context"; accountBinding?: string; context: DreaminaScopedTaskContext };
// These values are a compatibility journal contract, not a second product lifecycle.
export type DreaminaRuntimeJournalState =
| "queued"
| "pending"
| "accepted"
| "succeeded"
| "failed"
| "cancelled"
| "unknown"
| "deleted";
export type DreaminaRuntimeJournalSnapshot = {
state: DreaminaRuntimeJournalState;
submitId?: string;
receiptRecorded?: boolean;
errorCode?: string;
officialStatus?: DreaminaProviderObservation["status"];
};
const FORWARD_TRANSITIONS: Readonly<Record<DreaminaTaskLifecycle, readonly DreaminaTaskLifecycle[]>> = {
QUEUED_LOCAL: ["SUBMITTING"],
SUBMITTING: ["ACCEPTED", "SUBMISSION_UNCERTAIN", "TERMINAL"],
SUBMISSION_UNCERTAIN: ["ACCEPTED", "TERMINAL"],
ACCEPTED: ["RUNNING", "TERMINAL"],
RUNNING: ["TERMINAL"],
TERMINAL: [],
};
export class DreaminaTaskTransitionError extends Error {
constructor(message: string) {
super(message);
this.name = "DreaminaTaskTransitionError";
}
}
export function reduceDreaminaTask(state: DreaminaTaskState, event: DreaminaTaskEvent): DreaminaTaskState {
if (event.type === "transition") return transitionLifecycle(state, event.lifecycle, event.terminalOutcome);
if (event.type === "provider_observation") return applyProviderObservation(state, event.observation);
if (event.type === "sync_error") {
assertSafeErrorCode(event.code);
return {
...state,
syncState: event.errorKind === "account" ? "SYNC_BLOCKED_ACCOUNT" : "SYNC_RETRY_WAIT",
lastSyncErrorCode: event.code,
};
}
if (event.type === "sync_recovered") {
if (state.syncState === "SYNC_CONFLICT") return state;
return { ...state, syncState: "SYNC_OK", lastSyncErrorCode: undefined };
}
return bindContext(state, event);
}
export function dreaminaTaskFromRuntimeJournal(input: DreaminaRuntimeJournalSnapshot): DreaminaTaskState {
const base: DreaminaTaskState = {
lifecycle: "QUEUED_LOCAL",
syncState: "SYNC_OK",
resultState: "NOT_AVAILABLE",
outputs: [],
context: { scope: "legacy_unscoped" },
};
if (input.state === "queued") return base;
if (input.state === "pending") return { ...base, lifecycle: "SUBMITTING" };
if (input.state === "unknown") {
return {
...base,
lifecycle: "SUBMISSION_UNCERTAIN",
syncState: "SYNC_UNCERTAIN",
...(input.errorCode ? { lastSyncErrorCode: input.errorCode } : {}),
};
}
if (input.state === "accepted") {
return {
...base,
lifecycle: input.officialStatus === "processing" ? "RUNNING" : "ACCEPTED",
...(input.errorCode === "dreamina_account_session_changed"
? { syncState: "SYNC_BLOCKED_ACCOUNT" as const, lastSyncErrorCode: input.errorCode }
: input.errorCode && isLocalSyncErrorCode(input.errorCode)
? { syncState: "SYNC_RETRY_WAIT" as const, lastSyncErrorCode: input.errorCode }
: {}),
};
}
if (input.state === "succeeded") {
return {
...base,
lifecycle: "TERMINAL",
terminalOutcome: "SUCCEEDED",
resultState: "PENDING_MATERIALIZATION",
...(input.errorCode === "dreamina_account_session_changed"
? { syncState: "SYNC_BLOCKED_ACCOUNT" as const, lastSyncErrorCode: input.errorCode }
: {}),
};
}
if (input.state === "cancelled") {
if (hasProviderTask(input) && input.errorCode === "dreamina_local_wait_stopped") {
return { ...base, lifecycle: "ACCEPTED" };
}
return { ...base, lifecycle: "TERMINAL", terminalOutcome: "CANCELLED" };
}
if (input.state === "failed") {
if (hasProviderTask(input) && input.errorCode && isLocalSyncErrorCode(input.errorCode)) {
return {
...base,
lifecycle: "ACCEPTED",
syncState: "SYNC_RETRY_WAIT",
lastSyncErrorCode: input.errorCode,
};
}
return {
...base,
lifecycle: "TERMINAL",
terminalOutcome: isAmbiguousOfficialFailure(input) ? "FAILED_OR_CANCELLED" : "FAILED",
};
}
return { ...base, lifecycle: "TERMINAL", terminalOutcome: "FAILED" };
}
function transitionLifecycle(
state: DreaminaTaskState,
lifecycle: DreaminaTaskLifecycle,
terminalOutcome?: DreaminaTerminalOutcome,
): DreaminaTaskState {
if (lifecycle === state.lifecycle) {
if (lifecycle !== "TERMINAL") {
if (terminalOutcome !== undefined) throw lifecycleError(state.lifecycle, lifecycle);
return state;
}
if (!terminalOutcome || terminalOutcome !== state.terminalOutcome) throw lifecycleError(state.lifecycle, lifecycle);
return state;
}
if (!FORWARD_TRANSITIONS[state.lifecycle].includes(lifecycle)) throw lifecycleError(state.lifecycle, lifecycle);
if (lifecycle === "TERMINAL" && !terminalOutcome) throw lifecycleError(state.lifecycle, lifecycle);
if (lifecycle !== "TERMINAL" && terminalOutcome !== undefined) throw lifecycleError(state.lifecycle, lifecycle);
if (state.lifecycle === "SUBMITTING" && lifecycle === "TERMINAL" && terminalOutcome !== "REJECTED") {
throw lifecycleError(state.lifecycle, lifecycle);
}
if (state.lifecycle === "SUBMISSION_UNCERTAIN" && lifecycle === "TERMINAL") {
throw lifecycleError(state.lifecycle, lifecycle);
}
const next: DreaminaTaskState = {
...state,
lifecycle,
terminalOutcome: lifecycle === "TERMINAL" ? terminalOutcome : undefined,
};
if (lifecycle === "SUBMISSION_UNCERTAIN") {
next.syncState = "SYNC_UNCERTAIN";
} else if (state.lifecycle === "SUBMISSION_UNCERTAIN" && lifecycle === "ACCEPTED") {
next.syncState = "SYNC_OK";
next.lastSyncErrorCode = undefined;
}
if (lifecycle === "TERMINAL" && terminalOutcome === "SUCCEEDED" && next.resultState === "NOT_AVAILABLE") {
next.resultState = "PENDING_MATERIALIZATION";
}
return next;
}
function applyProviderObservation(state: DreaminaTaskState, observation: DreaminaProviderObservation): DreaminaTaskState {
assertObservation(observation);
if (state.accountBinding && observation.accountBinding && state.accountBinding !== observation.accountBinding) {
return { ...state, syncState: "SYNC_CONFLICT" };
}
const withObservation: DreaminaTaskState = {
...state,
...(state.accountBinding || !observation.accountBinding ? {} : { accountBinding: observation.accountBinding }),
providerObservation: observation,
syncState: state.syncState === "SYNC_CONFLICT" ? "SYNC_CONFLICT" : "SYNC_OK",
lastSyncErrorCode: undefined,
};
const observedOutcome = terminalOutcomeForObservation(observation.status);
if (state.lifecycle === "TERMINAL") {
if (!observedOutcome || observedOutcome === state.terminalOutcome) return withObservation;
return { ...withObservation, syncState: "SYNC_CONFLICT", terminalOutcome: state.terminalOutcome };
}
if (observedOutcome) {
return transitionLifecycleFromObservation(withObservation, observedOutcome);
}
if (observation.status === "processing") {
if (state.lifecycle === "RUNNING") return withObservation;
return { ...withObservation, lifecycle: "RUNNING", terminalOutcome: undefined };
}
if (state.lifecycle === "RUNNING") return withObservation;
if (state.lifecycle === "ACCEPTED") return withObservation;
return { ...withObservation, lifecycle: "ACCEPTED", terminalOutcome: undefined };
}
function transitionLifecycleFromObservation(state: DreaminaTaskState, outcome: DreaminaTerminalOutcome): DreaminaTaskState {
const next: DreaminaTaskState = {
...state,
lifecycle: "TERMINAL",
terminalOutcome: outcome,
};
if (outcome === "SUCCEEDED" && next.resultState === "NOT_AVAILABLE") next.resultState = "PENDING_MATERIALIZATION";
return next;
}
function bindContext(
state: DreaminaTaskState,
event: Extract<DreaminaTaskEvent, { type: "bind_context" }>,
): DreaminaTaskState {
if (state.context.scope === "legacy_unscoped") return state;
if (event.accountBinding && state.accountBinding && event.accountBinding !== state.accountBinding) {
return { ...state, syncState: "SYNC_CONFLICT" };
}
const context = { ...state.context };
for (const key of ["projectId", "nodeId", "conversationId", "messageId", "batchIndex", "batchCount", "retryOf", "attemptGroupId"] as const) {
const incoming = event.context[key];
const current = context[key];
if (incoming === undefined) continue;
if (current !== undefined && current !== incoming) return { ...state, syncState: "SYNC_CONFLICT" };
Object.assign(context, { [key]: incoming });
}
return {
...state,
context,
...(state.accountBinding || !event.accountBinding ? {} : { accountBinding: event.accountBinding }),
};
}
function terminalOutcomeForObservation(status: DreaminaProviderObservation["status"]): DreaminaTerminalOutcome | undefined {
if (status === "completed") return "SUCCEEDED";
if (status === "cancelled") return "CANCELLED";
if (status === "failed") return "FAILED_OR_CANCELLED";
return undefined;
}
function hasProviderTask(input: DreaminaRuntimeJournalSnapshot) {
return Boolean(input.submitId) || input.receiptRecorded === true;
}
function isAmbiguousOfficialFailure(input: DreaminaRuntimeJournalSnapshot) {
return input.officialStatus === "failed"
|| input.errorCode === "dreamina_official_failed"
|| input.errorCode === "dreamina_official_incomplete";
}
function isLocalSyncErrorCode(code: string) {
return /(?:query|transport|parse|state_busy|state_fenced|login|session|unknown|origin_not_trusted)/.test(code)
&& !/^dreamina_official_/.test(code);
}
function assertObservation(observation: DreaminaProviderObservation) {
if (!Number.isFinite(Date.parse(observation.observedAt))) throw new DreaminaTaskTransitionError("Dreamina provider observation timestamp is invalid");
if (observation.fenceEpoch !== undefined && (!Number.isSafeInteger(observation.fenceEpoch) || observation.fenceEpoch < 0)) {
throw new DreaminaTaskTransitionError("Dreamina provider observation fence epoch is invalid");
}
}
function assertSafeErrorCode(code: string) {
if (!/^[a-z][a-z0-9_]{2,80}$/.test(code)) throw new DreaminaTaskTransitionError("Dreamina sync error code is invalid");
}
function lifecycleError(from: DreaminaTaskLifecycle, to: DreaminaTaskLifecycle) {
return new DreaminaTaskTransitionError(`Dreamina task lifecycle transition ${from} -> ${to} is forbidden`);
}