Gateway: discriminated protocol schema + CLI updates

This commit is contained in:
Peter Steinberger
2025-12-09 15:01:13 +01:00
parent 2746efeb25
commit 172ce6c79f
23 changed files with 2001 additions and 477 deletions

View File

@@ -1,46 +1,60 @@
import AjvPkg, { type ErrorObject } from "ajv";
import {
type AgentEvent,
AgentEventSchema,
AgentParamsSchema,
ErrorCodes,
ErrorShapeSchema,
EventFrameSchema,
HelloErrorSchema,
HelloOkSchema,
HelloSchema,
PresenceEntrySchema,
ProtocolSchemas,
RequestFrameSchema,
ResponseFrameSchema,
SendParamsSchema,
SnapshotSchema,
StateVersionSchema,
errorShape,
type AgentEvent,
type ErrorShape,
ErrorShapeSchema,
type EventFrame,
EventFrameSchema,
errorShape,
type Hello,
type HelloError,
HelloErrorSchema,
type HelloOk,
HelloOkSchema,
HelloSchema,
type PresenceEntry,
PresenceEntrySchema,
ProtocolSchemas,
PROTOCOL_VERSION,
type RequestFrame,
RequestFrameSchema,
type ResponseFrame,
ResponseFrameSchema,
SendParamsSchema,
type Snapshot,
SnapshotSchema,
type StateVersion,
StateVersionSchema,
TickEventSchema,
type TickEvent,
GatewayFrameSchema,
type GatewayFrame,
type ShutdownEvent,
ShutdownEventSchema,
} from "./schema.js";
const ajv = new (AjvPkg as unknown as new (opts?: object) => import("ajv").default)({
const ajv = new (
AjvPkg as unknown as new (
opts?: object,
) => import("ajv").default
)({
allErrors: true,
strict: false,
removeAdditional: false,
});
export const validateHello = ajv.compile<Hello>(HelloSchema);
export const validateRequestFrame = ajv.compile<RequestFrame>(RequestFrameSchema);
export const validateRequestFrame =
ajv.compile<RequestFrame>(RequestFrameSchema);
export const validateSendParams = ajv.compile(SendParamsSchema);
export const validateAgentParams = ajv.compile(AgentParamsSchema);
export function formatValidationErrors(errors: ErrorObject[] | null | undefined) {
export function formatValidationErrors(
errors: ErrorObject[] | null | undefined,
) {
if (!errors) return "unknown validation error";
return ajv.errorsText(errors, { separator: "; " });
}
@@ -52,6 +66,7 @@ export {
RequestFrameSchema,
ResponseFrameSchema,
EventFrameSchema,
GatewayFrameSchema,
PresenceEntrySchema,
SnapshotSchema,
ErrorShapeSchema,
@@ -59,12 +74,16 @@ export {
AgentEventSchema,
SendParamsSchema,
AgentParamsSchema,
TickEventSchema,
ShutdownEventSchema,
ProtocolSchemas,
PROTOCOL_VERSION,
ErrorCodes,
errorShape,
};
export type {
GatewayFrame,
Hello,
HelloOk,
HelloError,
@@ -76,4 +95,6 @@ export type {
ErrorShape,
StateVersion,
AgentEvent,
TickEvent,
ShutdownEvent,
};

View File

@@ -1,4 +1,4 @@
import { Type, type Static, type TSchema } from "@sinclair/typebox";
import { type Static, type TSchema, Type } from "@sinclair/typebox";
const NonEmptyString = Type.String({ minLength: 1 });
@@ -38,6 +38,21 @@ export const SnapshotSchema = Type.Object(
{ additionalProperties: false },
);
export const TickEventSchema = Type.Object(
{
ts: Type.Integer({ minimum: 0 }),
},
{ additionalProperties: false },
);
export const ShutdownEventSchema = Type.Object(
{
reason: NonEmptyString,
restartExpectedMs: Type.Optional(Type.Integer({ minimum: 0 })),
},
{ additionalProperties: false },
);
export const HelloSchema = Type.Object(
{
type: Type.Literal("hello"),
@@ -154,6 +169,21 @@ export const EventFrameSchema = Type.Object(
{ additionalProperties: false },
);
// Discriminated union of all top-level frames. Using a discriminator makes
// downstream codegen (quicktype) produce tighter types instead of all-optional
// blobs.
export const GatewayFrameSchema = Type.Union(
[
HelloSchema,
HelloOkSchema,
HelloErrorSchema,
RequestFrameSchema,
ResponseFrameSchema,
EventFrameSchema,
],
{ discriminator: "type" },
);
export const AgentEventSchema = Type.Object(
{
runId: NonEmptyString,
@@ -196,6 +226,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
RequestFrame: RequestFrameSchema,
ResponseFrame: ResponseFrameSchema,
EventFrame: EventFrameSchema,
GatewayFrame: GatewayFrameSchema,
PresenceEntry: PresenceEntrySchema,
StateVersion: StateVersionSchema,
Snapshot: SnapshotSchema,
@@ -203,19 +234,26 @@ export const ProtocolSchemas: Record<string, TSchema> = {
AgentEvent: AgentEventSchema,
SendParams: SendParamsSchema,
AgentParams: AgentParamsSchema,
TickEvent: TickEventSchema,
ShutdownEvent: ShutdownEventSchema,
};
export const PROTOCOL_VERSION = 1 as const;
export type Hello = Static<typeof HelloSchema>;
export type HelloOk = Static<typeof HelloOkSchema>;
export type HelloError = Static<typeof HelloErrorSchema>;
export type RequestFrame = Static<typeof RequestFrameSchema>;
export type ResponseFrame = Static<typeof ResponseFrameSchema>;
export type EventFrame = Static<typeof EventFrameSchema>;
export type GatewayFrame = Static<typeof GatewayFrameSchema>;
export type Snapshot = Static<typeof SnapshotSchema>;
export type PresenceEntry = Static<typeof PresenceEntrySchema>;
export type ErrorShape = Static<typeof ErrorShapeSchema>;
export type StateVersion = Static<typeof StateVersionSchema>;
export type AgentEvent = Static<typeof AgentEventSchema>;
export type TickEvent = Static<typeof TickEventSchema>;
export type ShutdownEvent = Static<typeof ShutdownEventSchema>;
export const ErrorCodes = {
NOT_LINKED: "NOT_LINKED",