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 }) { 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"; }