108 lines
4.5 KiB
TypeScript
108 lines
4.5 KiB
TypeScript
import { ApplicationFailure, Context } from "@temporalio/activity";
|
|
|
|
import type { WorkbenchActivityConfig, WorkbenchApplication, WorkbenchEventPublisher, WorkbenchTurnInput } from "./contracts.ts";
|
|
|
|
export function createWorkbenchActivities(application: WorkbenchApplication, options: {
|
|
activity: WorkbenchActivityConfig;
|
|
eventPublisher?: WorkbenchEventPublisher | null;
|
|
attempt?: () => number;
|
|
}) {
|
|
return {
|
|
async dispatchWorkbenchTurn(input: WorkbenchTurnInput) {
|
|
try {
|
|
return await application.dispatchTurn(input);
|
|
} catch (error) {
|
|
const attempt = positiveInteger(options.attempt?.() ?? currentActivityAttempt(), 1);
|
|
const retryable = (error as any)?.retryable !== false;
|
|
const exhausted = !retryable || attempt >= options.activity.retryMaximumAttempts;
|
|
await publishDispatchFailure(options.eventPublisher, input, error, {
|
|
attempt,
|
|
maxAttempts: options.activity.retryMaximumAttempts,
|
|
retryable,
|
|
exhausted,
|
|
backoffMs: exhausted ? null : retryDelayMs(options.activity, attempt)
|
|
});
|
|
throw ApplicationFailure.create({
|
|
message: errorMessage(error),
|
|
type: errorCode(error),
|
|
nonRetryable: !retryable,
|
|
details: [{
|
|
code: errorCode(error),
|
|
layer: (error as any)?.layer ?? null,
|
|
status: (error as any)?.status ?? null,
|
|
retryable,
|
|
attempt,
|
|
maxAttempts: options.activity.retryMaximumAttempts,
|
|
valuesPrinted: false
|
|
}]
|
|
});
|
|
}
|
|
},
|
|
async cancelWorkbenchTurn(input: { actor: WorkbenchTurnInput["actor"]; traceId: string; params: Record<string, unknown> }) {
|
|
return application.cancelTurn(input);
|
|
}
|
|
};
|
|
}
|
|
|
|
async function publishDispatchFailure(publisher: WorkbenchEventPublisher | null | undefined, input: WorkbenchTurnInput, error: unknown, state: { attempt: number; maxAttempts: number; retryable: boolean; exhausted: boolean; backoffMs: number | null }) {
|
|
if (!publisher) return;
|
|
const code = errorCode(error);
|
|
const message = userErrorMessage(error);
|
|
const createdAt = new Date().toISOString();
|
|
await publisher.publish({
|
|
traceId: input.traceId,
|
|
sessionId: input.sessionId,
|
|
event: {
|
|
sourceEventId: `workbench:${input.traceId}:dispatch-failure:${state.attempt}`,
|
|
type: state.exhausted ? "error" : "backend_status",
|
|
phase: state.exhausted ? "dispatch-failed" : "dispatch-retry-scheduled",
|
|
failureDomain: (error as any)?.layer === "billing" ? "billing" : "infrastructure",
|
|
component: "hwlab-cloud-api",
|
|
code,
|
|
failureKind: code,
|
|
summary: message,
|
|
message,
|
|
retryable: state.retryable,
|
|
willRetry: !state.exhausted,
|
|
retryPhase: state.exhausted ? "retryExhausted" : "retryScheduled",
|
|
attempt: state.attempt,
|
|
maxAttempts: state.maxAttempts,
|
|
backoffMs: state.backoffMs,
|
|
createdAt,
|
|
terminal: false,
|
|
valuesPrinted: false
|
|
}
|
|
});
|
|
if (!state.exhausted) return;
|
|
await publisher.publish({
|
|
traceId: input.traceId,
|
|
sessionId: input.sessionId,
|
|
event: {
|
|
sourceEventId: `workbench:${input.traceId}:terminal-failed`,
|
|
type: "terminal_status",
|
|
terminalStatus: "failed",
|
|
status: "failed",
|
|
failureDomain: (error as any)?.layer === "billing" ? "billing" : "infrastructure",
|
|
code,
|
|
failureKind: code,
|
|
errorCode: code,
|
|
message,
|
|
retryable: state.retryable,
|
|
attempt: state.attempt,
|
|
maxAttempts: state.maxAttempts,
|
|
createdAt,
|
|
terminal: true,
|
|
valuesPrinted: false
|
|
}
|
|
});
|
|
}
|
|
|
|
function currentActivityAttempt() {
|
|
try { return Context.current().info.attempt; } catch { return 1; }
|
|
}
|
|
function retryDelayMs(activity: WorkbenchActivityConfig, attempt: number) { return Math.min(activity.retryMaximumIntervalMs, activity.retryInitialIntervalMs * (2 ** Math.max(0, attempt - 1))); }
|
|
function positiveInteger(value: unknown, fallback: number) { const parsed = Number(value); return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback; }
|
|
function errorCode(error: unknown) { return String((error as any)?.code ?? "workbench_activity_failed").trim() || "workbench_activity_failed"; }
|
|
function errorMessage(error: unknown) { return String((error as any)?.message ?? "Workbench activity failed").trim() || "Workbench activity failed"; }
|
|
function userErrorMessage(error: unknown) { return String((error as any)?.userMessage ?? (error as any)?.message ?? "Workbench activity failed").trim() || "Workbench activity failed"; }
|