feat(protocol): add shared WebSocket contract package
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
"start": "node src/index.js",
|
||||
"build": "node -e \"console.log('vela-gateway: no build step required')\""
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "^5.2.1"
|
||||
}
|
||||
"dependencies": {
|
||||
"@vela/protocol": "0.0.0",
|
||||
"fastify": "^5.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const Fastify = require('fastify');
|
||||
const {
|
||||
CLIENT_EVENT_TYPES,
|
||||
PROTOCOL_PACKAGE_NAME,
|
||||
SERVER_EVENT_TYPES
|
||||
} = require('@vela/protocol');
|
||||
|
||||
function buildServer() {
|
||||
const app = Fastify({ logger: true });
|
||||
@@ -7,7 +12,12 @@ function buildServer() {
|
||||
service: 'vela-gateway',
|
||||
status: 'ok',
|
||||
transport: 'http',
|
||||
next: 'websocket session skeleton'
|
||||
next: 'websocket session skeleton',
|
||||
protocol: {
|
||||
package: PROTOCOL_PACKAGE_NAME,
|
||||
clientEventCount: CLIENT_EVENT_TYPES.length,
|
||||
serverEventCount: SERVER_EVENT_TYPES.length
|
||||
}
|
||||
}));
|
||||
|
||||
app.get('/health', async () => ({ status: 'ok' }));
|
||||
|
||||
13
apps/vela-protocol/package.json
Normal file
13
apps/vela-protocol/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@vela/protocol",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"import": "./src/index.js",
|
||||
"require": "./src/index.cjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/vela-protocol/src/index.cjs
Normal file
52
apps/vela-protocol/src/index.cjs
Normal file
@@ -0,0 +1,52 @@
|
||||
const PROTOCOL_PACKAGE_NAME = '@vela/protocol';
|
||||
|
||||
const SESSION_STATES = Object.freeze(['idle', 'listening', 'thinking', 'speaking']);
|
||||
|
||||
const CLIENT_EVENT_TYPES = Object.freeze([
|
||||
'session.start',
|
||||
'input_audio.append',
|
||||
'input_audio.commit',
|
||||
'response.cancel'
|
||||
]);
|
||||
|
||||
const SERVER_EVENT_TYPES = Object.freeze([
|
||||
'session.ready',
|
||||
'session.state',
|
||||
'transcript.partial',
|
||||
'transcript.final',
|
||||
'response.text.delta',
|
||||
'response.completed',
|
||||
'error'
|
||||
]);
|
||||
|
||||
function createMessageEnvelope(type, payload) {
|
||||
return { type, payload };
|
||||
}
|
||||
|
||||
function isMessageEnvelope(value) {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
'payload' in value
|
||||
);
|
||||
}
|
||||
|
||||
function isClientEventType(type) {
|
||||
return CLIENT_EVENT_TYPES.includes(type);
|
||||
}
|
||||
|
||||
function isServerEventType(type) {
|
||||
return SERVER_EVENT_TYPES.includes(type);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROTOCOL_PACKAGE_NAME,
|
||||
SESSION_STATES,
|
||||
CLIENT_EVENT_TYPES,
|
||||
SERVER_EVENT_TYPES,
|
||||
createMessageEnvelope,
|
||||
isMessageEnvelope,
|
||||
isClientEventType,
|
||||
isServerEventType
|
||||
};
|
||||
68
apps/vela-protocol/src/index.d.ts
vendored
Normal file
68
apps/vela-protocol/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
export type SessionState = 'idle' | 'listening' | 'thinking' | 'speaking';
|
||||
|
||||
export type MessageEnvelope<TType extends string, TPayload> = {
|
||||
type: TType;
|
||||
payload: TPayload;
|
||||
};
|
||||
|
||||
export type ClientEventPayloads = {
|
||||
'session.start': Record<string, never>;
|
||||
'input_audio.append': {
|
||||
chunk: string;
|
||||
};
|
||||
'input_audio.commit': Record<string, never>;
|
||||
'response.cancel': Record<string, never>;
|
||||
};
|
||||
|
||||
export type ServerEventPayloads = {
|
||||
'session.ready': {
|
||||
sessionId: string;
|
||||
};
|
||||
'session.state': {
|
||||
value: SessionState;
|
||||
};
|
||||
'transcript.partial': {
|
||||
text: string;
|
||||
};
|
||||
'transcript.final': {
|
||||
text: string;
|
||||
};
|
||||
'response.text.delta': {
|
||||
text: string;
|
||||
};
|
||||
'response.completed': Record<string, never>;
|
||||
'error': {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type ClientEventType = keyof ClientEventPayloads;
|
||||
export type ServerEventType = keyof ServerEventPayloads;
|
||||
|
||||
export type ClientEvent = {
|
||||
[Type in ClientEventType]: MessageEnvelope<Type, ClientEventPayloads[Type]>;
|
||||
}[ClientEventType];
|
||||
|
||||
export type ServerEvent = {
|
||||
[Type in ServerEventType]: MessageEnvelope<Type, ServerEventPayloads[Type]>;
|
||||
}[ServerEventType];
|
||||
|
||||
export const PROTOCOL_PACKAGE_NAME: '@vela/protocol';
|
||||
export const SESSION_STATES: readonly SessionState[];
|
||||
export const CLIENT_EVENT_TYPES: readonly ClientEventType[];
|
||||
export const SERVER_EVENT_TYPES: readonly ServerEventType[];
|
||||
|
||||
export function createMessageEnvelope<TType extends ClientEventType>(
|
||||
type: TType,
|
||||
payload: ClientEventPayloads[TType]
|
||||
): MessageEnvelope<TType, ClientEventPayloads[TType]>;
|
||||
export function createMessageEnvelope<TType extends ServerEventType>(
|
||||
type: TType,
|
||||
payload: ServerEventPayloads[TType]
|
||||
): MessageEnvelope<TType, ServerEventPayloads[TType]>;
|
||||
|
||||
export function isMessageEnvelope(value: unknown): value is MessageEnvelope<string, unknown>;
|
||||
export function isClientEventType(type: string): type is ClientEventType;
|
||||
export function isServerEventType(type: string): type is ServerEventType;
|
||||
41
apps/vela-protocol/src/index.js
Normal file
41
apps/vela-protocol/src/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export const PROTOCOL_PACKAGE_NAME = '@vela/protocol';
|
||||
|
||||
export const SESSION_STATES = Object.freeze(['idle', 'listening', 'thinking', 'speaking']);
|
||||
|
||||
export const CLIENT_EVENT_TYPES = Object.freeze([
|
||||
'session.start',
|
||||
'input_audio.append',
|
||||
'input_audio.commit',
|
||||
'response.cancel'
|
||||
]);
|
||||
|
||||
export const SERVER_EVENT_TYPES = Object.freeze([
|
||||
'session.ready',
|
||||
'session.state',
|
||||
'transcript.partial',
|
||||
'transcript.final',
|
||||
'response.text.delta',
|
||||
'response.completed',
|
||||
'error'
|
||||
]);
|
||||
|
||||
export function createMessageEnvelope(type, payload) {
|
||||
return { type, payload };
|
||||
}
|
||||
|
||||
export function isMessageEnvelope(value) {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
'payload' in value
|
||||
);
|
||||
}
|
||||
|
||||
export function isClientEventType(type) {
|
||||
return CLIENT_EVENT_TYPES.includes(type);
|
||||
}
|
||||
|
||||
export function isServerEventType(type) {
|
||||
return SERVER_EVENT_TYPES.includes(type);
|
||||
}
|
||||
@@ -10,10 +10,11 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/kit": "^2.17.1",
|
||||
"svelte": "^5.19.5"
|
||||
"dependencies": {
|
||||
"@vela/protocol": "0.0.0",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/kit": "^2.17.1",
|
||||
"svelte": "^5.19.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
|
||||
@@ -7,8 +7,14 @@
|
||||
</svelte:head>
|
||||
|
||||
<script>
|
||||
import {
|
||||
CLIENT_EVENT_TYPES,
|
||||
PROTOCOL_PACKAGE_NAME,
|
||||
SERVER_EVENT_TYPES
|
||||
} from '@vela/protocol';
|
||||
|
||||
const appStatus = 'Bootstrapped';
|
||||
const nextFocus = 'Wire the voice session contract to the gateway.';
|
||||
const nextFocus = `Build the voice session shell on top of ${PROTOCOL_PACKAGE_NAME}.`;
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
@@ -20,6 +26,11 @@
|
||||
streaming session UI will be added in later increments.
|
||||
</p>
|
||||
|
||||
<p class="contract-note">
|
||||
Shared protocol package loaded with {CLIENT_EVENT_TYPES.length} client event types and
|
||||
{SERVER_EVENT_TYPES.length} server event types.
|
||||
</p>
|
||||
|
||||
<div class="meta">
|
||||
<div>
|
||||
<span>Status</span>
|
||||
@@ -76,6 +87,10 @@
|
||||
color: #c7d6e8;
|
||||
}
|
||||
|
||||
.contract-note {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 1.5rem;
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user