| 1 | import * as Schema from "effect/Schema"; |
| 2 | |
| 3 | import { |
| 4 | CommandId, |
| 5 | ForwardCompatibleArray, |
| 6 | IsoDateTime, |
| 7 | ProjectId, |
| 8 | ScheduledTaskId, |
| 9 | SecretRef, |
| 10 | ThreadId, |
| 11 | TrimmedNonEmptyString, |
| 12 | } from "./baseSchemas.ts"; |
| 13 | import { ModelSelection } from "./modelSelection.ts"; |
| 14 | import { |
| 15 | OrchestrationV2Actor, |
| 16 | OrchestrationV2CreationSource, |
| 17 | OrchestrationV2ThreadLaunchWorkspaceStrategy, |
| 18 | } from "./orchestrationV2.ts"; |
| 19 | import { ProviderInteractionMode, RuntimeMode } from "./providerPolicy.ts"; |
| 20 | |
| 21 | /** 24-hour "HH:MM" wall-clock time. Mirrors `parseTimeOfDay` on the server. */ |
| 22 | const TimeOfDay = TrimmedNonEmptyString.check( |
| 23 | Schema.isPattern(/^([01]?\d|2[0-3]):([0-5]\d)$/), |
| 24 | ).annotate({ description: "Local wall-clock time in 24-hour HH:MM form, such as 09:30." }); |
| 25 | |
| 26 | export const MIN_SCHEDULED_TASK_INTERVAL_MS = 60_000; |
| 27 | |
| 28 | const ScheduledTaskIntervalMs = Schema.Int.check(Schema.isGreaterThan(0)).annotate({ |
| 29 | description: "Positive interval in milliseconds.", |
| 30 | }); |
| 31 | |
| 32 | const ScheduledTaskIntervalSchedule = Schema.Struct({ |
| 33 | type: Schema.Literal("interval").annotate({ |
| 34 | description: "Select interval scheduling.", |
| 35 | }), |
| 36 | everyMs: ScheduledTaskIntervalMs, |
| 37 | }).annotate({ |
| 38 | description: "Run repeatedly after a fixed number of milliseconds.", |
| 39 | }); |
| 40 | |
| 41 | const ScheduledTaskFixedTimeSchedule = Schema.Struct({ |
| 42 | type: Schema.Literal("fixed_time").annotate({ |
| 43 | description: "Select a fixed local wall-clock time.", |
| 44 | }), |
| 45 | timeOfDay: TimeOfDay, |
| 46 | weekdays: Schema.optional( |
| 47 | Schema.Array( |
| 48 | Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 6 })).annotate({ |
| 49 | description: "Weekday number where 0 is Sunday and 6 is Saturday.", |
| 50 | }), |
| 51 | ).annotate({ |
| 52 | description: "Optional weekdays; omit to run every day.", |
| 53 | }), |
| 54 | ), |
| 55 | }).annotate({ |
| 56 | description: "Run at a fixed local wall-clock time on selected weekdays.", |
| 57 | }); |
| 58 | |
| 59 | const ScheduledTaskWebhookSignatureFields = { |
| 60 | header: TrimmedNonEmptyString.annotate({ |
| 61 | description: "Request header carrying the signature, such as x-hub-signature-256.", |
| 62 | }), |
| 63 | encoding: Schema.Literals(["hex", "base64"]).annotate({ |
| 64 | description: "How the HMAC-SHA256 digest is encoded in the header.", |
| 65 | }), |
| 66 | prefix: Schema.String.annotate({ |
| 67 | description: "Text before the digest in the header value, such as 'sha256='. Empty for none.", |
| 68 | }), |
| 69 | }; |
| 70 | |
| 71 | /** HMAC-SHA256 over the raw request body. The secret is never part of the read model. */ |
| 72 | export const ScheduledTaskWebhookSignature = Schema.Struct( |
| 73 | ScheduledTaskWebhookSignatureFields, |
| 74 | ).annotate({ description: "Optional HMAC-SHA256 signature check over the raw request body." }); |
| 75 | export type ScheduledTaskWebhookSignature = typeof ScheduledTaskWebhookSignature.Type; |
| 76 | |
| 77 | /** Matches how long the relay holds a request for an offline environment. */ |
| 78 | export const MAX_WEBHOOK_DELIVERY_AGE_MINUTES = 24 * 60; |
| 79 | |
| 80 | const WebhookMaxDeliveryAgeMinutes = Schema.Int.check( |
| 81 | Schema.isBetween({ minimum: 1, maximum: MAX_WEBHOOK_DELIVERY_AGE_MINUTES }), |
| 82 | ).annotate({ |
| 83 | description: |
| 84 | "Skip requests the relay held longer than this many minutes while the environment was offline. Null runs every request.", |
| 85 | }); |
| 86 | |
| 87 | const ScheduledTaskWebhookSchedule = Schema.Struct({ |
| 88 | type: Schema.Literal("webhook").annotate({ |
| 89 | description: "Run when the task's webhook URL receives a request.", |
| 90 | }), |
| 91 | signature: Schema.NullOr(ScheduledTaskWebhookSignature), |
| 92 | // Optional so rows saved before this setting existed still decode. |
| 93 | maxDeliveryAgeMinutes: Schema.optional(Schema.NullOr(WebhookMaxDeliveryAgeMinutes)), |
| 94 | }).annotate({ |
| 95 | description: |
| 96 | "Run on each request to the task's webhook URL. The prompt may use {{body.path}}, {{headers.name}}, {{query.name}}, {{body}} and {{request}} placeholders.", |
| 97 | }); |
| 98 | |
| 99 | const ScheduledTaskUpsertWebhookSchedule = Schema.Struct({ |
| 100 | type: Schema.Literal("webhook").annotate({ |
| 101 | description: "Run when the task's webhook URL receives a request.", |
| 102 | }), |
| 103 | signature: Schema.optional( |
| 104 | Schema.NullOr( |
| 105 | Schema.Struct({ |
| 106 | ...ScheduledTaskWebhookSignatureFields, |
| 107 | secret: Schema.optional(TrimmedNonEmptyString).annotate({ |
| 108 | description: "Shared signing secret. Omit to keep the stored secret.", |
| 109 | }), |
| 110 | secretRef: Schema.optional(SecretRef).annotate({ |
| 111 | description: |
| 112 | "A secret the user entered through request_secret, used instead of secret. It is consumed by this save.", |
| 113 | }), |
| 114 | }), |
| 115 | ), |
| 116 | ).annotate({ |
| 117 | description: "Signature check; omit or null to accept requests by URL token only.", |
| 118 | }), |
| 119 | maxDeliveryAgeMinutes: Schema.optional(Schema.NullOr(WebhookMaxDeliveryAgeMinutes)), |
| 120 | }).annotate({ |
| 121 | description: |
| 122 | "Run on each request to the task's webhook URL. The prompt may use {{body.path}}, {{headers.name}}, {{query.name}}, {{body}} and {{request}} placeholders.", |
| 123 | }); |
| 124 | |
| 125 | /** |
| 126 | * Read model for persisted schedules. Keep accepting legacy sub-minute rows so |
| 127 | * users can list, disable, edit, or delete them after the write minimum changes. |
| 128 | */ |
| 129 | export const ScheduledTaskSchedule = Schema.Union([ |
| 130 | ScheduledTaskIntervalSchedule, |
| 131 | ScheduledTaskFixedTimeSchedule, |
| 132 | ScheduledTaskWebhookSchedule, |
| 133 | ]).annotate({ |
| 134 | description: |
| 135 | "Structured trigger. Pass an object with type 'interval', 'fixed_time' or 'webhook'.", |
| 136 | }); |
| 137 | export type ScheduledTaskSchedule = typeof ScheduledTaskSchedule.Type; |
| 138 | |
| 139 | /** Mutation model: newly created or updated interval schedules run at most once per minute. */ |
| 140 | export const ScheduledTaskUpsertSchedule = Schema.Union([ |
| 141 | Schema.Struct({ |
| 142 | type: Schema.Literal("interval").annotate({ |
| 143 | description: "Select interval scheduling.", |
| 144 | }), |
| 145 | everyMs: ScheduledTaskIntervalMs.check( |
| 146 | Schema.isGreaterThanOrEqualTo(MIN_SCHEDULED_TASK_INTERVAL_MS), |
| 147 | ).annotate({ |
| 148 | description: "Interval in milliseconds, with a minimum of 60000 (one minute).", |
| 149 | }), |
| 150 | }).annotate({ |
| 151 | description: "Run repeatedly after a fixed number of milliseconds.", |
| 152 | }), |
| 153 | ScheduledTaskFixedTimeSchedule, |
| 154 | ScheduledTaskUpsertWebhookSchedule, |
| 155 | ]).annotate({ |
| 156 | description: "Writable trigger. Pass an object with type 'interval', 'fixed_time' or 'webhook'.", |
| 157 | }); |
| 158 | export type ScheduledTaskUpsertSchedule = typeof ScheduledTaskUpsertSchedule.Type; |
| 159 | |
| 160 | export const ScheduledTaskRunStatus = Schema.Literals(["never", "running", "succeeded", "failed"]); |
| 161 | export type ScheduledTaskRunStatus = typeof ScheduledTaskRunStatus.Type; |
| 162 | |
| 163 | /** Where a webhook task receives requests. Present only on webhook tasks. */ |
| 164 | export const ScheduledTaskWebhookEndpoint = Schema.Struct({ |
| 165 | /** Environment-relative path including the secret token; works on any origin that reaches the environment. */ |
| 166 | path: TrimmedNonEmptyString, |
| 167 | /** Public T3 Connect URL, or null when the environment is not linked to T3 Connect. */ |
| 168 | url: Schema.NullOr(TrimmedNonEmptyString), |
| 169 | hasSecret: Schema.Boolean, |
| 170 | }); |
| 171 | export type ScheduledTaskWebhookEndpoint = typeof ScheduledTaskWebhookEndpoint.Type; |
| 172 | |
| 173 | export const ScheduledTask = Schema.Struct({ |
| 174 | id: ScheduledTaskId, |
| 175 | title: TrimmedNonEmptyString, |
| 176 | prompt: TrimmedNonEmptyString, |
| 177 | enabled: Schema.Boolean, |
| 178 | schedule: ScheduledTaskSchedule, |
| 179 | projectId: ProjectId, |
| 180 | threadId: Schema.NullOr(ThreadId), |
| 181 | workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy, |
| 182 | modelSelection: ModelSelection, |
| 183 | runtimeMode: RuntimeMode, |
| 184 | interactionMode: ProviderInteractionMode, |
| 185 | createdBy: OrchestrationV2Actor, |
| 186 | creationSource: OrchestrationV2CreationSource, |
| 187 | createdAt: IsoDateTime, |
| 188 | updatedAt: IsoDateTime, |
| 189 | nextRunAt: Schema.NullOr(IsoDateTime), |
| 190 | lastRunAt: Schema.NullOr(IsoDateTime), |
| 191 | lastRunStatus: ScheduledTaskRunStatus, |
| 192 | lastRunError: Schema.NullOr(Schema.String), |
| 193 | runCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 194 | webhook: Schema.optional(ScheduledTaskWebhookEndpoint), |
| 195 | }); |
| 196 | export type ScheduledTask = typeof ScheduledTask.Type; |
| 197 | |
| 198 | export const ScheduledTaskListInput = Schema.Struct({}); |
| 199 | export type ScheduledTaskListInput = typeof ScheduledTaskListInput.Type; |
| 200 | |
| 201 | export const ScheduledTaskListResult = Schema.Struct({ |
| 202 | // Trigger types grow over time; a client must not lose the whole list over |
| 203 | // one task it cannot decode. |
| 204 | tasks: ForwardCompatibleArray(ScheduledTask), |
| 205 | }); |
| 206 | export type ScheduledTaskListResult = typeof ScheduledTaskListResult.Type; |
| 207 | |
| 208 | export const ScheduledTaskUpsertInput = Schema.Struct({ |
| 209 | id: Schema.optional(ScheduledTaskId), |
| 210 | requireExisting: Schema.optional(Schema.Boolean).annotate({ |
| 211 | description: "Reject the save if the task no longer exists, for edits from a client form.", |
| 212 | }), |
| 213 | commandId: Schema.optional(CommandId), |
| 214 | title: TrimmedNonEmptyString, |
| 215 | prompt: TrimmedNonEmptyString, |
| 216 | enabled: Schema.Boolean, |
| 217 | schedule: ScheduledTaskUpsertSchedule, |
| 218 | projectId: ProjectId, |
| 219 | threadId: Schema.optional(Schema.NullOr(ThreadId)), |
| 220 | workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy, |
| 221 | modelSelection: ModelSelection, |
| 222 | runtimeMode: RuntimeMode, |
| 223 | interactionMode: ProviderInteractionMode, |
| 224 | createdBy: Schema.optional(OrchestrationV2Actor), |
| 225 | creationSource: Schema.optional(OrchestrationV2CreationSource), |
| 226 | }); |
| 227 | export type ScheduledTaskUpsertInput = typeof ScheduledTaskUpsertInput.Type; |
| 228 | |
| 229 | /** Partial update that flips only the enabled flag — never overwrites other fields. */ |
| 230 | export const ScheduledTaskSetEnabledInput = Schema.Struct({ |
| 231 | id: ScheduledTaskId, |
| 232 | enabled: Schema.Boolean, |
| 233 | }); |
| 234 | export type ScheduledTaskSetEnabledInput = typeof ScheduledTaskSetEnabledInput.Type; |
| 235 | |
| 236 | export const ScheduledTaskDeleteInput = Schema.Struct({ |
| 237 | id: ScheduledTaskId, |
| 238 | }); |
| 239 | export type ScheduledTaskDeleteInput = typeof ScheduledTaskDeleteInput.Type; |
| 240 | |
| 241 | export const ScheduledTaskRunNowInput = Schema.Struct({ |
| 242 | id: ScheduledTaskId, |
| 243 | }); |
| 244 | export type ScheduledTaskRunNowInput = typeof ScheduledTaskRunNowInput.Type; |
| 245 | |
| 246 | export const ScheduledTaskRotateWebhookTokenInput = Schema.Struct({ |
| 247 | id: ScheduledTaskId, |
| 248 | }); |
| 249 | export type ScheduledTaskRotateWebhookTokenInput = typeof ScheduledTaskRotateWebhookTokenInput.Type; |
| 250 | |
| 251 | export const ScheduledTaskWebhookDeliveryId = TrimmedNonEmptyString.pipe( |
| 252 | Schema.brand("ScheduledTaskWebhookDeliveryId"), |
| 253 | ); |
| 254 | export type ScheduledTaskWebhookDeliveryId = typeof ScheduledTaskWebhookDeliveryId.Type; |
| 255 | |
| 256 | export const ScheduledTaskWebhookDeliveryOutcome = Schema.Literals([ |
| 257 | "accepted", |
| 258 | "dispatch_failed", |
| 259 | "rejected_signature", |
| 260 | "disabled", |
| 261 | "rate_limited", |
| 262 | "expired", |
| 263 | ]); |
| 264 | export type ScheduledTaskWebhookDeliveryOutcome = typeof ScheduledTaskWebhookDeliveryOutcome.Type; |
| 265 | |
| 266 | export const ScheduledTaskWebhookDeliverySummary = Schema.Struct({ |
| 267 | id: ScheduledTaskWebhookDeliveryId, |
| 268 | taskId: ScheduledTaskId, |
| 269 | receivedAt: IsoDateTime, |
| 270 | method: TrimmedNonEmptyString, |
| 271 | contentType: Schema.NullOr(Schema.String), |
| 272 | bodyBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), |
| 273 | outcome: ScheduledTaskWebhookDeliveryOutcome, |
| 274 | /** True when a configured signature matched; false when none was configured. */ |
| 275 | signatureVerified: Schema.Boolean, |
| 276 | /** Template placeholders that had no value in this request and rendered empty. */ |
| 277 | missingFields: Schema.Array(Schema.String), |
| 278 | error: Schema.NullOr(Schema.String), |
| 279 | }); |
| 280 | export type ScheduledTaskWebhookDeliverySummary = typeof ScheduledTaskWebhookDeliverySummary.Type; |
| 281 | |
| 282 | export const ScheduledTaskWebhookDelivery = Schema.Struct({ |
| 283 | ...ScheduledTaskWebhookDeliverySummary.fields, |
| 284 | query: Schema.String, |
| 285 | headers: Schema.Record(Schema.String, Schema.String), |
| 286 | /** Body as UTF-8 text, cut at the log limit; see bodyTruncated. */ |
| 287 | body: Schema.String, |
| 288 | bodyTruncated: Schema.Boolean, |
| 289 | renderedPrompt: Schema.NullOr(Schema.String), |
| 290 | }); |
| 291 | export type ScheduledTaskWebhookDelivery = typeof ScheduledTaskWebhookDelivery.Type; |
| 292 | |
| 293 | export const ScheduledTaskListWebhookDeliveriesInput = Schema.Struct({ |
| 294 | id: ScheduledTaskId, |
| 295 | }); |
| 296 | export type ScheduledTaskListWebhookDeliveriesInput = |
| 297 | typeof ScheduledTaskListWebhookDeliveriesInput.Type; |
| 298 | |
| 299 | export const ScheduledTaskListWebhookDeliveriesResult = Schema.Struct({ |
| 300 | deliveries: Schema.Array(ScheduledTaskWebhookDeliverySummary), |
| 301 | }); |
| 302 | export type ScheduledTaskListWebhookDeliveriesResult = |
| 303 | typeof ScheduledTaskListWebhookDeliveriesResult.Type; |
| 304 | |
| 305 | export const ScheduledTaskGetWebhookDeliveryInput = Schema.Struct({ |
| 306 | id: ScheduledTaskId, |
| 307 | deliveryId: ScheduledTaskWebhookDeliveryId, |
| 308 | }); |
| 309 | export type ScheduledTaskGetWebhookDeliveryInput = typeof ScheduledTaskGetWebhookDeliveryInput.Type; |
| 310 | |
| 311 | export const ScheduledTaskGetWebhookDeliveryResult = Schema.Struct({ |
| 312 | delivery: ScheduledTaskWebhookDelivery, |
| 313 | }); |
| 314 | export type ScheduledTaskGetWebhookDeliveryResult = |
| 315 | typeof ScheduledTaskGetWebhookDeliveryResult.Type; |
| 316 | |
| 317 | export const ScheduledTaskMutationResult = Schema.Struct({ |
| 318 | task: ScheduledTask, |
| 319 | }); |
| 320 | export type ScheduledTaskMutationResult = typeof ScheduledTaskMutationResult.Type; |
| 321 | |
| 322 | export const ScheduledTaskDeleteResult = Schema.Struct({ |
| 323 | id: ScheduledTaskId, |
| 324 | }); |
| 325 | export type ScheduledTaskDeleteResult = typeof ScheduledTaskDeleteResult.Type; |
| 326 | |
| 327 | export const ScheduledTaskRunNowResult = Schema.Struct({ |
| 328 | task: ScheduledTask, |
| 329 | }); |
| 330 | export type ScheduledTaskRunNowResult = typeof ScheduledTaskRunNowResult.Type; |
| 331 | |
| 332 | export class ScheduledTaskError extends Schema.TaggedError<ScheduledTaskError>()( |
| 333 | "ScheduledTaskError", |
| 334 | { |
| 335 | message: Schema.String, |
| 336 | taskId: Schema.optional(ScheduledTaskId), |
| 337 | cause: Schema.optional(Schema.Defect()), |
| 338 | }, |
| 339 | ) {} |