import { z } from "zod/v4"; import { colineUiNodeSchema } from "../tree/tree"; import { toolInvokeCapabilitiesSchema } from "./tool-invoke"; /** * Wire payloads for the execution kinds added after Phase 6, carried in * the protocol-2 envelope. Each one is dispatched by the platform to the * app's logic — the hosted Dynamic Worker or the external backend's * `/coline/app` handler — with a per-run invocation token. */ const actorSchema = z.discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("user"), userId: z.string().min(1).max(200) }), z.strictObject({ kind: z.literal("app") }), ]); export const TRIGGER_EVENT_KIND = "trigger.event" as const; export const triggerEventPayloadSchema = z.strictObject({ event: z.strictObject({ id: z.string().min(1).max(200), type: z.string().min(1).max(160), occurredAt: z.iso.datetime(), actor: actorSchema.nullable(), source: z.discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("platform") }), z.strictObject({ kind: z.literal("app"), appKey: z.string().min(1).max(64) }), ]), data: z.record(z.string(), z.unknown()), }), attempt: z.number().int().min(1), capabilities: toolInvokeCapabilitiesSchema, }); export type TriggerEventPayload = z.infer; export const ENDPOINT_REQUEST_KIND = "endpoint.request" as const; export const endpointRequestPayloadSchema = z.strictObject({ request: z.strictObject({ endpointId: z.string().min(1).max(80), method: z.string().min(1).max(10), path: z.string().min(1).max(2000), query: z.record(z.string(), z.string()), headers: z.record(z.string(), z.string()), body: z.string(), bodyBase64: z.string().nullable(), receivedAt: z.iso.datetime(), verifiedBy: z.string().min(1).max(40), }), capabilities: toolInvokeCapabilitiesSchema, }); export type EndpointRequestPayload = z.infer; export const endpointResponseSchema = z.strictObject({ status: z.number().int().min(100).max(599).default(200), headers: z.record(z.string().max(120), z.string().max(4000)).default({}), body: z.unknown().optional(), }); export type EndpointResponse = z.infer; export const JOB_RUN_KIND = "job.run" as const; export const jobRunPayloadSchema = z.strictObject({ job: z.strictObject({ id: z.string().min(1).max(200), name: z.string().min(1).max(80), attempt: z.number().int().min(1), maxAttempts: z.number().int().min(1), enqueuedAt: z.iso.datetime(), checkpoint: z.unknown(), payload: z.record(z.string(), z.unknown()), }), actor: actorSchema, capabilities: toolInvokeCapabilitiesSchema, }); export type JobRunPayload = z.infer; export const jobRunResultSchema = z.union([ z.strictObject({ kind: z.literal("done"), result: z.unknown().optional() }), z.strictObject({ kind: z.literal("continue"), payload: z.record(z.string(), z.unknown()), delaySeconds: z.number().int().min(0).max(86_400).optional(), }), ]); export type JobRunResult = z.infer; export const RENDER_PUBLIC_KIND = "render.public" as const; export const renderPublicPayloadSchema = z.strictObject({ path: z.string().min(1).max(2000), query: z.record(z.string(), z.string()), visitor: z.strictObject({ id: z.string().min(1).max(200) }), capabilities: toolInvokeCapabilitiesSchema, }); export type RenderPublicPayload = z.infer; export const renderPublicResultSchema = z.strictObject({ tree: colineUiNodeSchema }); /** Convert a job's return value into the wire result. */ export function toJobRunResult(outcome: unknown): JobRunResult { if (outcome && typeof outcome === "object" && "continueWith" in outcome) { const value = outcome as { continueWith: Record; delaySeconds?: number }; return { kind: "continue", payload: value.continueWith, ...(value.delaySeconds !== undefined ? { delaySeconds: value.delaySeconds } : {}), }; } if (outcome && typeof outcome === "object" && "result" in outcome) { return { kind: "done", result: (outcome as { result?: unknown }).result }; } return { kind: "done" }; } /** Normalize a handler's endpoint response for the wire. */ export function toEndpointResponse(value: unknown): EndpointResponse { if (!value || typeof value !== "object") return { status: 200, headers: {} }; const parsed = endpointResponseSchema.safeParse(value); if (!parsed.success) { throw Object.assign(new Error("Endpoint handler returned an invalid response."), { code: "invalid_response", }); } return parsed.data; }