diff --git a/packages/plugin-protocol/README.md b/packages/plugin-protocol/README.md new file mode 100644 index 000000000..92576d67e --- /dev/null +++ b/packages/plugin-protocol/README.md @@ -0,0 +1,31 @@ +# @proj-airi/plugin-protocol + +Shared protocol contracts for plugin-module communication in Project AIRI. + +## What it does + +- Defines websocket event names and payload types for module/plugin orchestration. +- Exposes Eventa event definitions bound to protocol event names. +- Provides shared transport/event utility types used by server and plugin runtimes. + +## How to use + +```ts +import type { WebSocketEvent, WebSocketEventOf, WebSocketEvents } from '@proj-airi/plugin-protocol/types' + +import { moduleAnnounce, moduleAuthenticate } from '@proj-airi/plugin-protocol/types' +``` + +## When to use + +- You need canonical protocol contracts for plugin <-> host communication. +- You need event name stability and matching payload definitions across runtimes. + +## When not to use + +- You only need higher-level runtime client APIs from SDK packages. +- You are implementing app-only UI state that is not part of plugin/server transport contracts. + +## License + +[MIT](../../LICENSE) diff --git a/packages/plugin-protocol/package.json b/packages/plugin-protocol/package.json new file mode 100644 index 000000000..e64db8790 --- /dev/null +++ b/packages/plugin-protocol/package.json @@ -0,0 +1,39 @@ +{ + "name": "@proj-airi/plugin-protocol", + "type": "module", + "version": "0.8.4", + "description": "Plugin protocol event definitions and shared websocket types for Project AIRI", + "author": { + "name": "Moeru AI Project AIRI Team", + "email": "airi@moeru.ai", + "url": "https://github.com/moeru-ai" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/moeru-ai/airi.git", + "directory": "packages/plugin-protocol" + }, + "exports": { + "./types": { + "types": "./dist/types/index.d.mts", + "default": "./dist/types/index.mjs" + } + }, + "main": "./dist/types/index.mjs", + "types": "./dist/types/index.d.mts", + "files": [ + "README.md", + "dist", + "package.json" + ], + "scripts": { + "dev": "pnpm run build", + "build": "tsdown", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@moeru/eventa": "catalog:", + "@xsai/shared-chat": "catalog:" + } +} diff --git a/packages/plugin-protocol/src/index.ts b/packages/plugin-protocol/src/index.ts new file mode 100644 index 000000000..f768d9ee3 --- /dev/null +++ b/packages/plugin-protocol/src/index.ts @@ -0,0 +1 @@ +console.warn('import @proj-airi/plugin-protocol/types instead') diff --git a/packages/plugin-protocol/src/types/events.ts b/packages/plugin-protocol/src/types/events.ts new file mode 100644 index 000000000..932df740a --- /dev/null +++ b/packages/plugin-protocol/src/types/events.ts @@ -0,0 +1,1042 @@ +import type { AssistantMessage, CommonContentPart, Message, ToolMessage, UserMessage } from '@xsai/shared-chat' + +import { defineEventa } from '@moeru/eventa' + +export interface DiscordGuildMember { + nickname: string + displayName: string + id: string +} + +export interface Discord { + guildMember?: DiscordGuildMember + guildId?: string + guildName?: string + channelId?: string +} + +export interface PluginIdentity { + /** + * Stable plugin identifier (shared across instances). + * Example: "telegram-bot", "stage-tamagotchi". + */ + id: string + /** + * Optional semantic version for the plugin. + * Example: "0.8.1-beta.7". + */ + version?: string + /** + * Optional labels attached to the plugin manifest. + * Example: { env: "prod", app: "telegram", devtools: "true" }. + */ + labels?: Record +} + +export interface ModuleIdentity { + /** + * Unique module instance id for this module run (per process/deployment). + * Example: "telegram-01", "stage-ui-2f7c9". + */ + id: string + /** + * Module identity kind. For now only plugin-backed modules are supported. + */ + kind: 'plugin' + /** + * Plugin identity associated with this module instance. + */ + plugin: PluginIdentity + /** + * K8s-style labels for routing and policy selectors. + * Example: { env: "prod", app: "telegram", devtools: "true" }. + */ + labels?: Record +} + +export type MetadataEventSource = ModuleIdentity + +/** + * Static schema metadata for module configuration. + * This is transport-friendly and can be paired with a JSON Schema-like object. + * + * Example: + * { + * id: "airi.config.stage-ui", + * version: 2, + * schema: { type: "object", properties: { model: { type: "string" } }, required: ["model"] }, + * } + */ +export interface ModuleConfigSchema { + id: string + version: number + /** + * Optional JSON Schema-like descriptor for tooling/validation. + * Keep it JSON-serializable and avoid runtime-only values. + */ + schema?: Record +} + +/** + * Module dependency declaration. + * + * Use this during prepare/probe to describe what a module needs before + * it can decide its dynamic contributions. Dependencies can change at + * runtime if peers go offline. + * + * Example: + * { role: "llm:orchestrator", min: "v1", optional: true } + */ +export interface ModuleDependency { + /** + * Logical dependency role (preferred over hard-coded plugin ids). + * Example: "llm:orchestrator" + */ + role: string + /** + * Optional dependency flag. + */ + optional?: boolean + /** + * Version constraint hints. + */ + version?: string + min?: string + max?: string + /** + * Additional constraint metadata (JSON-serializable). + */ + constraints?: Record +} + +/** + * Dynamic contributions emitted by a module after configuration. + * + * Unlike static manifests, contributions can be updated or revoked at + * runtime. This is where capabilities, provider registrations, and UI + * extensions should be declared. + * + * Example: + * { + * capabilities: ["context.aggregate"], + * providers: [{ id: "vscode-context", type: "context-source" }], + * ui: { widgets: ["context-summary-panel"] } + * } + */ +export interface ModuleContribution { + /** + * Dynamic capabilities exposed by the module. + */ + capabilities?: string[] + /** + * Provider registry contributions (shape defined by the host). + */ + providers?: Array> + /** + * UI contribution descriptors (widgets, toolbar items, etc). + */ + ui?: Record + /** + * Hook registrations (event handlers, interceptors, etc). + */ + hooks?: Array> + /** + * Additional resources or metadata. + */ + resources?: Record +} + +/** + * Lifecycle phases for module orchestration and UX. + */ +export type ModulePhase + = | 'announced' + | 'preparing' + | 'prepared' + | 'configuration-needed' + | 'configured' + | 'ready' + | 'failed' + +export type Localizable + = | string + | { + /** + * Localization key owned by the module. + * Example: "config.deprecated.model_driver.legacy" + */ + key: string + /** + * Fallback display string when translation is unavailable. + */ + fallback?: string + /** + * Params for string interpolation. + */ + params?: Record + } + +export interface ModuleConfigNotice { + /** + * Machine-friendly key for analytics or client-side mapping. + */ + code?: string + /** + * Human readable message or localization key. + */ + message?: Localizable + /** + * JSON pointer or dotted path in config. + * Example: "driver.legacyModelPath" + */ + path?: string + /** + * Suggested replacement path or alternative. + */ + replacedBy?: string + /** + * Version since the notice applies. + */ + since?: number + /** + * Link to docs or migration guide. + */ + link?: string +} + +export interface ModuleConfigStep { + /** + * Suggested action to complete configuration. + * Use code for UI rendering or message for fallback. + */ + code?: string + message?: Localizable + /** + * Optional targeted field(s). + */ + paths?: string[] +} + +export interface ModuleConfigPlan { + /** + * Schema that this plan targets. + */ + schema: ModuleConfigSchema + /** + * Missing required paths for current schema/version. + */ + missing?: string[] + /** + * Invalid fields with reasons (runtime validation result). + */ + invalid?: Array<{ path: string, reason: string }> + /** + * Recommended defaults computed at runtime (may be environment-specific). + */ + defaults?: Record + /** + * Deprecated fields/behaviors detected in current config. + */ + deprecated?: Array + /** + * Suggested migration steps between schema versions. + */ + migrations?: Array<{ + from: number + to: number + steps?: Array + notes?: Array + }> + /** + * Human- or UI-friendly next actions to resolve partial config. + */ + nextSteps?: Array + /** + * Non-blocking issues that should be shown to the user/operator. + */ + warnings?: Array +} + +export interface ModuleConfigValidation { + /** + * Overall validation status. + * + * - valid: all required fields present and valid. + * - partial: config is structurally OK but missing required fields; can be fixed by patches. + * - invalid: one or more fields are present but invalid (type/range/format); requires correction. + */ + status: 'partial' | 'valid' | 'invalid' + /** + * Missing required fields (only for partial/invalid). + */ + missing?: string[] + /** + * Invalid fields with reasons (only for invalid). + */ + invalid?: Array<{ path: string, reason: Localizable }> + /** + * Non-blocking issues (e.g., deprecations, best-practice notices). + */ + warnings?: Array +} + +/** + * Config payload envelope for plan/apply/validate/commit. + * + * Example: + * { + * configId: "stage-ui-live2d", + * revision: 12, + * schemaVersion: 2, + * full: { model: "Hiyori", driver: { type: "live2d" } }, + * } + */ +export interface ModuleConfigEnvelope> { + configId: string + /** + * Monotonic revision number for this configId. + */ + revision: number + /** + * Schema version this config targets. + */ + schemaVersion: number + /** + * Optional source identity (who produced this config). + */ + source?: ModuleIdentity + /** + * Full config payload (use when first applying or rehydrating). + */ + full?: C + /** + * Partial patch payload (use when updating or filling missing fields). + */ + patch?: Partial + /** + * If patch is used, baseRevision should be set for optimistic concurrency. + */ + baseRevision?: number +} + +export interface ModuleCapability { + /** + * Stable capability id within a module. + * Example: "memory.write", "vision.ocr". + */ + id: string + /** + * Human-friendly name. + */ + name?: string + /** + * Optional localized description. + */ + description?: Localizable + /** + * Capability-specific config schema (if needed). + */ + configSchema?: ModuleConfigSchema + /** + * Additional metadata for tooling/UI. + */ + metadata?: Record +} + +export type RouteTargetExpression + = | { type: 'and', all: RouteTargetExpression[] } + | { type: 'or', any: RouteTargetExpression[] } + | { type: 'glob', glob: string, inverted?: boolean } + | { type: 'ids', ids: string[], inverted?: boolean } + | { type: 'plugin', plugins: string[], inverted?: boolean } + | { type: 'instance', instances: string[], inverted?: boolean } + | { type: 'label', selectors: string[], inverted?: boolean } + | { type: 'module', modules: string[], inverted?: boolean } + | { type: 'source', sources: string[], inverted?: boolean } + +export interface RouteConfig { + destinations?: Array + bypass?: boolean +} + +export enum MessageHeartbeatKind { + Ping = 'ping', + Pong = 'pong', +} + +export enum MessageHeartbeat { + Ping = '🩵', + Pong = '💛', +} + +export enum WebSocketEventSource { + Server = 'proj-airi:server-runtime', + StageWeb = 'proj-airi:stage-web', + StageTamagotchi = 'proj-airi:stage-tamagotchi', +} + +interface InputSource { + 'stage-web': boolean + 'stage-tamagotchi': boolean + 'discord': Discord +} + +interface OutputSource { + 'gen-ai:chat': { + message: UserMessage + contexts: Record, string | CommonContentPart[]>[]> + composedMessage: Array + input?: InputEventEnvelope + } +} + +export enum ContextUpdateStrategy { + ReplaceSelf = 'replace-self', + AppendSelf = 'append-self', +} + +export interface ContextUpdateDestinationAll { + all: true +} + +export interface ContextUpdateDestinationList { + include?: Array + exclude?: Array +} + +export type ContextUpdateDestinationFilter + = | ContextUpdateDestinationAll + | ContextUpdateDestinationList + +export interface ContextUpdate< + Metadata extends Record = Record, + // eslint-disable-next-line ts/no-unnecessary-type-constraint + Content extends any = undefined, +> { + id: string + /** + * Can be the same if same update sends multiple time as attempts + * and trials, (e.g. notified first but not ACKed, then retried). + */ + contextId: string + lane?: string + ideas?: Array + hints?: Array + strategy: ContextUpdateStrategy + text: string + content?: Content + destinations?: Array | ContextUpdateDestinationFilter + metadata?: Metadata +} + +export interface InputMessageOverrides { + sessionId?: string + messagePrefix?: string +} + +export type InputContextUpdate + = Omit, string | CommonContentPart[]>, 'id' | 'contextId'> + & Partial, string | CommonContentPart[]>, 'id' | 'contextId'>> + +export interface WebSocketEventInputTextBase { + text: string + textRaw?: string + overrides?: InputMessageOverrides + contextUpdates?: InputContextUpdate[] +} + +export type WebSocketEventInputText = WebSocketEventInputTextBase & Partial> + +export interface WebSocketEventInputTextVoiceBase { + transcription: string + textRaw?: string + overrides?: InputMessageOverrides + contextUpdates?: InputContextUpdate[] +} + +export type WebSocketEventInputTextVoice = WebSocketEventInputTextVoiceBase & Partial> + +export interface WebSocketEventInputVoiceBase { + audio: ArrayBuffer + overrides?: InputMessageOverrides + contextUpdates?: InputContextUpdate[] +} + +export type WebSocketEventInputVoice = WebSocketEventInputVoiceBase & Partial> + +export type InputEventData = WebSocketEventInputText | WebSocketEventInputTextVoice | WebSocketEventInputVoice + +export type InputEventEnvelope + = | { type: 'input:text', data: WebSocketEventInputText } + | { type: 'input:text:voice', data: WebSocketEventInputTextVoice } + | { type: 'input:voice', data: WebSocketEventInputVoice } + +export interface EventBaseMetadata { + source?: ModuleIdentity + event?: { + id?: string + parentId?: string + } +} + +export type WithInputSource = { + [S in Source]: InputSource[S] +} + +export type WithOutputSource = { + [S in Source]: OutputSource[S] +} + +// Module orchestration (local or remote transport): +// +// 1) module:authenticate → module:authenticated +// 2) registry:modules:sync (host → module bootstrap) +// 3) module:announce (identity, deps, config schema) +// 4) module:prepared +// 5) module:configuration:* (validate/plan/commit flow) +// 6) module:configuration:configured +// 7) module:contribute:capability:offer (repeat per capability) +// 8) module:contribute:capability:configuration:* (optional) +// 9) module:contribute:capability:activated +// 10) module:status (ready) +// 11) module:status:change (to re-run phases) + +interface ModuleAuthenticateEvent { + token: string +} + +interface ModuleAuthenticatedEvent { + authenticated: boolean +} + +interface ModuleCompatibilityRequestEvent { + protocolVersion: string + apiVersion: string + supportedProtocolVersions?: string[] + supportedApiVersions?: string[] +} + +interface ModuleCompatibilityResultEvent { + protocolVersion: string + apiVersion: string + mode: 'exact' | 'downgraded' | 'rejected' + reason?: string +} + +interface RegistryModulesSyncEvent { + modules: Array<{ + name: string + index?: number + identity: ModuleIdentity + }> +} + +interface ErrorEvent { + message: string +} + +interface ModuleAnnounceEvent { + name: string + identity: ModuleIdentity + possibleEvents: Array<(keyof ProtocolEvents)> + configSchema?: ModuleConfigSchema + dependencies?: ModuleDependency[] +} + +interface ModulePreparedEvent { + identity: ModuleIdentity + missingDependencies?: ModuleDependency[] +} + +interface ModuleConfigurationNeededEvent { + identity: ModuleIdentity + schema?: ModuleConfigSchema + current?: ModuleConfigEnvelope + reason?: string +} + +interface ModuleStatusEvent { + identity: ModuleIdentity + phase: ModulePhase + reason?: string + details?: Record +} + +interface ModuleConfigurationValidateRequestEvent { + identity: ModuleIdentity + current?: ModuleConfigEnvelope +} + +interface ModuleConfigurationValidateResponseEvent { + identity: ModuleIdentity + validation: ModuleConfigValidation + plan?: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleConfigurationValidateStatusEvent { + identity: ModuleIdentity + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleConfigurationPlanRequestEvent { + identity: ModuleIdentity + plan?: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleConfigurationPlanResponseEvent { + identity: ModuleIdentity + plan: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleConfigurationPlanStatusEvent { + identity: ModuleIdentity + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleConfigurationCommitEvent { + identity: ModuleIdentity + config: ModuleConfigEnvelope +} + +interface ModuleConfigurationCommitStatusEvent { + identity: ModuleIdentity + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleConfigurationConfiguredEvent { + identity: ModuleIdentity + config: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityOfferEvent { + identity: ModuleIdentity + capability: ModuleCapability +} + +interface ModuleContributeCapabilityConfigurationNeededEvent { + identity: ModuleIdentity + capabilityId: string + schema?: ModuleConfigSchema + current?: ModuleConfigEnvelope + reason?: string +} + +interface ModuleContributeCapabilityConfigurationValidateRequestEvent { + identity: ModuleIdentity + capabilityId: string + current?: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityConfigurationValidateResponseEvent { + identity: ModuleIdentity + capabilityId: string + validation: ModuleConfigValidation + plan?: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityConfigurationValidateStatusEvent { + identity: ModuleIdentity + capabilityId: string + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleContributeCapabilityConfigurationPlanRequestEvent { + identity: ModuleIdentity + capabilityId: string + plan?: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityConfigurationPlanResponseEvent { + identity: ModuleIdentity + capabilityId: string + plan: ModuleConfigPlan + current?: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityConfigurationPlanStatusEvent { + identity: ModuleIdentity + capabilityId: string + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleContributeCapabilityConfigurationCommitEvent { + identity: ModuleIdentity + capabilityId: string + config: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityConfigurationCommitStatusEvent { + identity: ModuleIdentity + capabilityId: string + state: 'queued' | 'working' | 'done' | 'failed' + note?: string + progress?: number +} + +interface ModuleContributeCapabilityConfigurationConfiguredEvent { + identity: ModuleIdentity + capabilityId: string + config: ModuleConfigEnvelope +} + +interface ModuleContributeCapabilityActivatedEvent { + identity: ModuleIdentity + capabilityId: string + active: boolean + reason?: string +} + +interface ModuleStatusChangeEvent { + identity: ModuleIdentity + phase: ModulePhase + reason?: string + details?: Record +} + +interface ModuleConfigureEvent { + config: C | Record +} + +interface UiConfigureEvent { + moduleName: string + moduleIndex?: number + config: C | Record +} + +type OutputGenAiChatToolCallEvent = { + toolCalls: ToolMessage[] +} & Partial> & Partial> + +type OutputGenAiChatMessageEvent = { + message: AssistantMessage +} & Partial> & Partial> + +interface OutputGenAiChatUsage { + promptTokens: number + completionTokens: number + totalTokens: number + source: 'provider-based' | 'estimate-based' +} + +type OutputGenAiChatCompleteEvent = { + message: AssistantMessage + toolCalls: ToolMessage[] + usage: OutputGenAiChatUsage +} & Partial> & Partial> + +interface SparkNotifyEvent { + id: string + eventId: string + lane?: string + kind: 'alarm' | 'ping' | 'reminder' + urgency: 'immediate' | 'soon' | 'later' + headline: string + note?: string + payload?: Record + ttlMs?: number + requiresAck?: boolean + destinations: Array + metadata?: Record +} + +interface SparkEmitEvent { + id: string + eventId?: string + state: 'queued' | 'working' | 'done' | 'dropped' | 'blocked' | 'expired' + note?: string + destinations: Array + metadata?: Record +} + +interface SparkCommandGuidanceOption { + label: string + steps: Array + rationale?: string + possibleOutcome?: Array + risk?: 'high' | 'medium' | 'low' | 'none' + fallback?: Array + triggers?: Array +} + +interface SparkCommandGuidance { + type: 'proposal' | 'instruction' | 'memory-recall' + /** + * Personas can be used to adjust the behavior of sub-agents. + * For example, when using as NPC in games, or player in Minecraft, + * the persona can help define the character's traits and decision-making style. + * + * Example: + * persona: { + * "bravery": "high", + * "cautiousness": "low", + * "friendliness": "medium" + * } + */ + persona?: Record + options: Array +} + +interface SparkCommandEvent { + id: string + eventId?: string + parentEventId?: string + commandId: string + interrupt: 'force' | 'soft' | false + priority: 'critical' | 'high' | 'normal' | 'low' + intent: 'plan' | 'proposal' | 'action' | 'pause' | 'resume' | 'reroute' | 'context' + ack?: string + guidance?: SparkCommandGuidance + contexts?: Array + destinations: Array +} + +interface TransportConnectionHeartbeatEvent { + kind: MessageHeartbeatKind + message: MessageHeartbeat | string + at?: number +} + +type ContextUpdateEvent = ContextUpdate + +export const moduleAuthenticate = defineEventa('module:authenticate') +export const moduleAuthenticated = defineEventa('module:authenticated') +export const moduleCompatibilityRequest = defineEventa('module:compatibility:request') +export const moduleCompatibilityResult = defineEventa('module:compatibility:result') +export const registryModulesSync = defineEventa('registry:modules:sync') + +export const error = defineEventa('error') + +export const moduleAnnounce = defineEventa('module:announce') +export const modulePrepared = defineEventa('module:prepared') +export const moduleConfigurationNeeded = defineEventa('module:configuration:needed') +export const moduleStatus = defineEventa('module:status') + +export const moduleConfigurationValidateRequest = defineEventa('module:configuration:validate:request') +export const moduleConfigurationValidateResponse = defineEventa('module:configuration:validate:response') +export const moduleConfigurationValidateStatus = defineEventa('module:configuration:validate:status') +export const moduleConfigurationPlanRequest = defineEventa('module:configuration:plan:request') +export const moduleConfigurationPlanResponse = defineEventa('module:configuration:plan:response') +export const moduleConfigurationPlanStatus = defineEventa('module:configuration:plan:status') +export const moduleConfigurationCommit = defineEventa('module:configuration:commit') +export const moduleConfigurationCommitStatus = defineEventa('module:configuration:commit:status') +export const moduleConfigurationConfigured = defineEventa('module:configuration:configured') + +export const moduleContributeCapabilityOffer = defineEventa('module:contribute:capability:offer') +export const moduleContributeCapabilityConfigurationNeeded = defineEventa('module:contribute:capability:configuration:needed') +export const moduleContributeCapabilityConfigurationValidateRequest = defineEventa('module:contribute:capability:configuration:validate:request') +export const moduleContributeCapabilityConfigurationValidateResponse = defineEventa('module:contribute:capability:configuration:validate:response') +export const moduleContributeCapabilityConfigurationValidateStatus = defineEventa('module:contribute:capability:configuration:validate:status') +export const moduleContributeCapabilityConfigurationPlanRequest = defineEventa('module:contribute:capability:configuration:plan:request') +export const moduleContributeCapabilityConfigurationPlanResponse = defineEventa('module:contribute:capability:configuration:plan:response') +export const moduleContributeCapabilityConfigurationPlanStatus = defineEventa('module:contribute:capability:configuration:plan:status') +export const moduleContributeCapabilityConfigurationCommit = defineEventa('module:contribute:capability:configuration:commit') +export const moduleContributeCapabilityConfigurationCommitStatus = defineEventa('module:contribute:capability:configuration:commit:status') +export const moduleContributeCapabilityConfigurationConfigured = defineEventa('module:contribute:capability:configuration:configured') +export const moduleContributeCapabilityActivated = defineEventa('module:contribute:capability:activated') + +export const moduleStatusChange = defineEventa('module:status:change') + +export const moduleConfigure = defineEventa('module:configure') + +export const uiConfigure = defineEventa('ui:configure') + +export const inputText = defineEventa('input:text') +export const inputTextVoice = defineEventa('input:text:voice') +export const inputVoice = defineEventa('input:voice') + +export const outputGenAiChatToolCall = defineEventa('output:gen-ai:chat:tool-call') +export const outputGenAiChatMessage = defineEventa('output:gen-ai:chat:message') +export const outputGenAiChatComplete = defineEventa('output:gen-ai:chat:complete') + +export const sparkNotify = defineEventa('spark:notify') +export const sparkEmit = defineEventa('spark:emit') +export const sparkCommand = defineEventa('spark:command') + +export const transportConnectionHeartbeat = defineEventa('transport:connection:heartbeat') +export const contextUpdate = defineEventa('context:update') + +// Thanks to: +// +// A little hack for creating extensible discriminated unions : r/typescript +// https://www.reddit.com/r/typescript/comments/1064ibt/a_little_hack_for_creating_extensible/ +export interface ProtocolEvents { + 'error': ErrorEvent + + 'module:authenticate': ModuleAuthenticateEvent + 'module:authenticated': ModuleAuthenticatedEvent + /** + * Plugin asks host to negotiate protocol + API compatibility. + */ + 'module:compatibility:request': ModuleCompatibilityRequestEvent + /** + * Host replies with accepted mode/result for protocol + API compatibility. + */ + 'module:compatibility:result': ModuleCompatibilityResultEvent + /** + * Server-side registry sync for known online modules. + * Sent to newly authenticated peers to bootstrap module discovery. + */ + 'registry:modules:sync': RegistryModulesSyncEvent + 'module:announce': ModuleAnnounceEvent + /** + * Prepare completed. Host can move into config apply/validate. + * + * Example: + * module:prepared { missingDependencies: [] } + */ + 'module:prepared': ModulePreparedEvent + /** + * Module needs configuration to proceed to prepared/configured. + */ + 'module:configuration:needed': ModuleConfigurationNeededEvent + /** + * Lifecycle status updates for orchestration/UX. + * + * Example: + * module:status { phase: "ready" } + */ + 'module:status': ModuleStatusEvent + /** + * Ask the module to validate current config (host → module). + */ + 'module:configuration:validate:request': ModuleConfigurationValidateRequestEvent + /** + * Validation response (module → host), with optional plan suggestions. + */ + 'module:configuration:validate:response': ModuleConfigurationValidateResponseEvent + /** + * Status updates for validation (module → host). + */ + 'module:configuration:validate:status': ModuleConfigurationValidateStatusEvent + /** + * Configuration planning request (host → module). + */ + 'module:configuration:plan:request': ModuleConfigurationPlanRequestEvent + /** + * Configuration planning response (module → host). + */ + 'module:configuration:plan:response': ModuleConfigurationPlanResponseEvent + /** + * Status updates for planning (module → host). + */ + 'module:configuration:plan:status': ModuleConfigurationPlanStatusEvent + /** + * Commit a config as "active" (host → module). + */ + 'module:configuration:commit': ModuleConfigurationCommitEvent + /** + * Status updates for commit (module → host). + */ + 'module:configuration:commit:status': ModuleConfigurationCommitStatusEvent + /** + * Configuration fully applied and active (module → host). + */ + 'module:configuration:configured': ModuleConfigurationConfiguredEvent + /** + * Capability offer emitted after module configuration. + */ + 'module:contribute:capability:offer': ModuleContributeCapabilityOfferEvent + /** + * Capability needs configuration before activation. + */ + 'module:contribute:capability:configuration:needed': ModuleContributeCapabilityConfigurationNeededEvent + 'module:contribute:capability:configuration:validate:request': ModuleContributeCapabilityConfigurationValidateRequestEvent + 'module:contribute:capability:configuration:validate:response': ModuleContributeCapabilityConfigurationValidateResponseEvent + 'module:contribute:capability:configuration:validate:status': ModuleContributeCapabilityConfigurationValidateStatusEvent + 'module:contribute:capability:configuration:plan:request': ModuleContributeCapabilityConfigurationPlanRequestEvent + 'module:contribute:capability:configuration:plan:response': ModuleContributeCapabilityConfigurationPlanResponseEvent + 'module:contribute:capability:configuration:plan:status': ModuleContributeCapabilityConfigurationPlanStatusEvent + 'module:contribute:capability:configuration:commit': ModuleContributeCapabilityConfigurationCommitEvent + 'module:contribute:capability:configuration:commit:status': ModuleContributeCapabilityConfigurationCommitStatusEvent + 'module:contribute:capability:configuration:configured': ModuleContributeCapabilityConfigurationConfiguredEvent + 'module:contribute:capability:activated': ModuleContributeCapabilityActivatedEvent + /** + * Request a phase transition (module → host). + */ + 'module:status:change': ModuleStatusChangeEvent + /** + * Push configuration down to module (host → module). + */ + 'module:configure': ModuleConfigureEvent + + 'ui:configure': UiConfigureEvent + + 'input:text': WebSocketEventInputText + 'input:text:voice': WebSocketEventInputTextVoice + 'input:voice': WebSocketEventInputVoice + + 'output:gen-ai:chat:tool-call': OutputGenAiChatToolCallEvent + 'output:gen-ai:chat:message': OutputGenAiChatMessageEvent + 'output:gen-ai:chat:complete': OutputGenAiChatCompleteEvent + + /** + * Spark used for allowing agents in a network to raise an event toward the other destinations (e.g. character). + * + * DO: + * - Use notify for episodic events (alarms/pings/reminders) with minimal payload. + * - Use command for high-level intent; let sub-agents translate into their own state machines. + * - Use emit for ack/progress/completion; include ids for tracing/dedupe. + * - Route via destinations; keep payloads small; use context:update for richer ideas. + * - Dedupe/log via id/eventId for observability. + * + * DOn't: + * - Stream high-frequency telemetry here (keep a separate channel). + * - Stuff large blobs into payload/contexts; prefer refs/summaries. + * - Assume exactly-once; add retry/ack on critical paths. You may rely on id/eventId for dedupe. + * - Allow untrusted agents to broadcast without auth/capability checks. + * + * Examples: + * - Minecraft attack/death: kind=alarm, urgency=immediate (fast bubble-up). + * e.g., fromAgent='minecraft', headline='Under attack by witch', payload includes hp/location/gear. + * - Cat bowl empty from HomeAssistant: kind=alarm, urgency=soon. + * - IM/email "read now": kind=ping, urgency=immediate. + * - Action Required email: kind=reminder, urgency=later. + * + * destinations controls routing (e.g. ['character'], ['character','minecraft-agent']). + */ + 'spark:notify': SparkNotifyEvent + + /** + * Acknowledgement/progress/state for a spark or command (bidirectional). + * Examples: + * - Character: state=working, note="Seen it, responding". + * - Sub-agent: state=done, note="Healed and safe". + * - Sub-agent: state=blocked/dropped with note when it cannot comply. + * - Minecraft: state=working, note="Pillared up; healing" in reply to a command. + */ + 'spark:emit': SparkEmitEvent + + /** + * Character issues instructions or context to a sub-agent. + * interrupt: force = hard preempt; soft = merge/queue. + * Examples: + * - Witch attack: interrupt=force, priority=critical, intent=action with options (aggressive/cautious). + * e.g., options to block/retreat vs push with shield/sword, with fallback steps. + * - Prep plan: interrupt=soft, priority=high, intent=plan with steps/fallbacks. + * - Contextual hints: intent=context with contextPatch ideas/hints. + */ + 'spark:command': SparkCommandEvent + + 'transport:connection:heartbeat': TransportConnectionHeartbeatEvent + + 'context:update': ContextUpdateEvent +} + +export type ProtocolEventOf = E extends keyof ProtocolEvents + ? Omit[E], 'metadata'> & { metadata?: Record } + : never diff --git a/packages/plugin-protocol/src/types/index.ts b/packages/plugin-protocol/src/types/index.ts new file mode 100644 index 000000000..def94ce45 --- /dev/null +++ b/packages/plugin-protocol/src/types/index.ts @@ -0,0 +1 @@ +export * from './events' diff --git a/packages/plugin-protocol/tsconfig.json b/packages/plugin-protocol/tsconfig.json new file mode 100644 index 000000000..00dcfd807 --- /dev/null +++ b/packages/plugin-protocol/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/plugin-protocol/tsdown.config.ts b/packages/plugin-protocol/tsdown.config.ts new file mode 100644 index 000000000..9fc144155 --- /dev/null +++ b/packages/plugin-protocol/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: { + 'types/index': 'src/types/index.ts', + }, + sourcemap: true, + unused: true, +}) diff --git a/packages/server-shared/package.json b/packages/server-shared/package.json index e304d076c..beb8c4db6 100644 --- a/packages/server-shared/package.json +++ b/packages/server-shared/package.json @@ -33,7 +33,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@moeru/eventa": "catalog:", - "@xsai/shared-chat": "catalog:" + "@proj-airi/plugin-protocol": "workspace:*" } } diff --git a/packages/server-shared/src/types/index.ts b/packages/server-shared/src/types/index.ts index b2196badb..7aaa85e9c 100644 --- a/packages/server-shared/src/types/index.ts +++ b/packages/server-shared/src/types/index.ts @@ -1 +1,2 @@ export * from './websocket' +export * from '@proj-airi/plugin-protocol/types' diff --git a/packages/server-shared/src/types/websocket/events.ts b/packages/server-shared/src/types/websocket/events.ts index 231e4a64e..05fe69087 100644 --- a/packages/server-shared/src/types/websocket/events.ts +++ b/packages/server-shared/src/types/websocket/events.ts @@ -1,472 +1,6 @@ -import type { AssistantMessage, CommonContentPart, Message, ToolMessage, UserMessage } from '@xsai/shared-chat' +import type { ModuleIdentity, ProtocolEvents, RouteConfig, WebSocketEventSource } from '@proj-airi/plugin-protocol/types' -import { defineEventa } from '@moeru/eventa' - -export interface DiscordGuildMember { - nickname: string - displayName: string - id: string -} - -export interface Discord { - guildMember?: DiscordGuildMember - guildId?: string - guildName?: string - channelId?: string -} - -export interface PluginIdentity { - /** - * Stable plugin identifier (shared across instances). - * Example: "telegram-bot", "stage-tamagotchi". - */ - id: string - /** - * Optional semantic version for the plugin. - * Example: "0.8.1-beta.7". - */ - version?: string - /** - * Optional labels attached to the plugin manifest. - * Example: { env: "prod", app: "telegram", devtools: "true" }. - */ - labels?: Record -} - -export interface ModuleIdentity { - /** - * Unique module instance id for this module run (per process/deployment). - * Example: "telegram-01", "stage-ui-2f7c9". - */ - id: string - /** - * Module identity kind. For now only plugin-backed modules are supported. - */ - kind: 'plugin' - /** - * Plugin identity associated with this module instance. - */ - plugin: PluginIdentity - /** - * K8s-style labels for routing and policy selectors. - * Example: { env: "prod", app: "telegram", devtools: "true" }. - */ - labels?: Record -} - -export type MetadataEventSource = ModuleIdentity - -/** - * Static schema metadata for module configuration. - * This is transport-friendly and can be paired with a JSON Schema-like object. - * - * Example: - * { - * id: "airi.config.stage-ui", - * version: 2, - * schema: { type: "object", properties: { model: { type: "string" } }, required: ["model"] }, - * } - */ -export interface ModuleConfigSchema { - id: string - version: number - /** - * Optional JSON Schema-like descriptor for tooling/validation. - * Keep it JSON-serializable and avoid runtime-only values. - */ - schema?: Record -} - -/** - * Module dependency declaration. - * - * Use this during prepare/probe to describe what a module needs before - * it can decide its dynamic contributions. Dependencies can change at - * runtime if peers go offline. - * - * Example: - * { role: "llm:orchestrator", min: "v1", optional: true } - */ -export interface ModuleDependency { - /** - * Logical dependency role (preferred over hard-coded plugin ids). - * Example: "llm:orchestrator" - */ - role: string - /** - * Optional dependency flag. - */ - optional?: boolean - /** - * Version constraint hints. - */ - version?: string - min?: string - max?: string - /** - * Additional constraint metadata (JSON-serializable). - */ - constraints?: Record -} - -/** - * Dynamic contributions emitted by a module after configuration. - * - * Unlike static manifests, contributions can be updated or revoked at - * runtime. This is where capabilities, provider registrations, and UI - * extensions should be declared. - * - * Example: - * { - * capabilities: ["context.aggregate"], - * providers: [{ id: "vscode-context", type: "context-source" }], - * ui: { widgets: ["context-summary-panel"] } - * } - */ -export interface ModuleContribution { - /** - * Dynamic capabilities exposed by the module. - */ - capabilities?: string[] - /** - * Provider registry contributions (shape defined by the host). - */ - providers?: Array> - /** - * UI contribution descriptors (widgets, toolbar items, etc). - */ - ui?: Record - /** - * Hook registrations (event handlers, interceptors, etc). - */ - hooks?: Array> - /** - * Additional resources or metadata. - */ - resources?: Record -} - -/** - * Lifecycle phases for module orchestration and UX. - */ -export type ModulePhase - = | 'announced' - | 'preparing' - | 'prepared' - | 'configuration-needed' - | 'configured' - | 'ready' - | 'failed' - -export type Localizable - = | string - | { - /** - * Localization key owned by the module. - * Example: "config.deprecated.model_driver.legacy" - */ - key: string - /** - * Fallback display string when translation is unavailable. - */ - fallback?: string - /** - * Params for string interpolation. - */ - params?: Record - } - -export interface ModuleConfigNotice { - /** - * Machine-friendly key for analytics or client-side mapping. - */ - code?: string - /** - * Human readable message or localization key. - */ - message?: Localizable - /** - * JSON pointer or dotted path in config. - * Example: "driver.legacyModelPath" - */ - path?: string - /** - * Suggested replacement path or alternative. - */ - replacedBy?: string - /** - * Version since the notice applies. - */ - since?: number - /** - * Link to docs or migration guide. - */ - link?: string -} - -export interface ModuleConfigStep { - /** - * Suggested action to complete configuration. - * Use code for UI rendering or message for fallback. - */ - code?: string - message?: Localizable - /** - * Optional targeted field(s). - */ - paths?: string[] -} - -export interface ModuleConfigPlan { - /** - * Schema that this plan targets. - */ - schema: ModuleConfigSchema - /** - * Missing required paths for current schema/version. - */ - missing?: string[] - /** - * Invalid fields with reasons (runtime validation result). - */ - invalid?: Array<{ path: string, reason: string }> - /** - * Recommended defaults computed at runtime (may be environment-specific). - */ - defaults?: Record - /** - * Deprecated fields/behaviors detected in current config. - */ - deprecated?: Array - /** - * Suggested migration steps between schema versions. - */ - migrations?: Array<{ - from: number - to: number - steps?: Array - notes?: Array - }> - /** - * Human- or UI-friendly next actions to resolve partial config. - */ - nextSteps?: Array - /** - * Non-blocking issues that should be shown to the user/operator. - */ - warnings?: Array -} - -export interface ModuleConfigValidation { - /** - * Overall validation status. - * - * - valid: all required fields present and valid. - * - partial: config is structurally OK but missing required fields; can be fixed by patches. - * - invalid: one or more fields are present but invalid (type/range/format); requires correction. - */ - status: 'partial' | 'valid' | 'invalid' - /** - * Missing required fields (only for partial/invalid). - */ - missing?: string[] - /** - * Invalid fields with reasons (only for invalid). - */ - invalid?: Array<{ path: string, reason: Localizable }> - /** - * Non-blocking issues (e.g., deprecations, best-practice notices). - */ - warnings?: Array -} - -/** - * Config payload envelope for plan/apply/validate/commit. - * - * Example: - * { - * configId: "stage-ui-live2d", - * revision: 12, - * schemaVersion: 2, - * full: { model: "Hiyori", driver: { type: "live2d" } }, - * } - */ -export interface ModuleConfigEnvelope> { - configId: string - /** - * Monotonic revision number for this configId. - */ - revision: number - /** - * Schema version this config targets. - */ - schemaVersion: number - /** - * Optional source identity (who produced this config). - */ - source?: ModuleIdentity - /** - * Full config payload (use when first applying or rehydrating). - */ - full?: C - /** - * Partial patch payload (use when updating or filling missing fields). - */ - patch?: Partial - /** - * If patch is used, baseRevision should be set for optimistic concurrency. - */ - baseRevision?: number -} - -export interface ModuleCapability { - /** - * Stable capability id within a module. - * Example: "memory.write", "vision.ocr". - */ - id: string - /** - * Human-friendly name. - */ - name?: string - /** - * Optional localized description. - */ - description?: Localizable - /** - * Capability-specific config schema (if needed). - */ - configSchema?: ModuleConfigSchema - /** - * Additional metadata for tooling/UI. - */ - metadata?: Record -} - -export type RouteTargetExpression - = | { type: 'and', all: RouteTargetExpression[] } - | { type: 'or', any: RouteTargetExpression[] } - | { type: 'glob', glob: string, inverted?: boolean } - | { type: 'ids', ids: string[], inverted?: boolean } - | { type: 'plugin', plugins: string[], inverted?: boolean } - | { type: 'instance', instances: string[], inverted?: boolean } - | { type: 'label', selectors: string[], inverted?: boolean } - | { type: 'module', modules: string[], inverted?: boolean } - | { type: 'source', sources: string[], inverted?: boolean } - -export interface RouteConfig { - destinations?: Array - bypass?: boolean -} - -export enum MessageHeartbeatKind { - Ping = 'ping', - Pong = 'pong', -} - -export enum MessageHeartbeat { - Ping = '🩵', - Pong = '💛', -} - -export enum WebSocketEventSource { - Server = 'proj-airi:server-runtime', - StageWeb = 'proj-airi:stage-web', - StageTamagotchi = 'proj-airi:stage-tamagotchi', -} - -interface InputSource { - 'stage-web': boolean - 'stage-tamagotchi': boolean - 'discord': Discord -} - -interface OutputSource { - 'gen-ai:chat': { - message: UserMessage - contexts: Record, string | CommonContentPart[]>[]> - composedMessage: Array - input?: WebSocketEventInputs - } -} - -export enum ContextUpdateStrategy { - ReplaceSelf = 'replace-self', - AppendSelf = 'append-self', -} - -export interface ContextUpdateDestinationAll { - all: true -} - -export interface ContextUpdateDestinationList { - include?: Array - exclude?: Array -} - -export type ContextUpdateDestinationFilter - = | ContextUpdateDestinationAll - | ContextUpdateDestinationList - -export interface ContextUpdate< - Metadata extends Record = Record, - // eslint-disable-next-line ts/no-unnecessary-type-constraint - Content extends any = undefined, -> { - id: string - /** - * Can be the same if same update sends multiple time as attempts - * and trials, (e.g. notified first but not ACKed, then retried). - */ - contextId: string - lane?: string - ideas?: Array - hints?: Array - strategy: ContextUpdateStrategy - text: string - content?: Content - destinations?: Array | ContextUpdateDestinationFilter - metadata?: Metadata -} - -export interface InputMessageOverrides { - sessionId?: string - messagePrefix?: string -} - -export type InputContextUpdate - = Omit, string | CommonContentPart[]>, 'id' | 'contextId'> - & Partial, string | CommonContentPart[]>, 'id' | 'contextId'>> - -export interface WebSocketEventInputTextBase { - text: string - textRaw?: string - overrides?: InputMessageOverrides - contextUpdates?: InputContextUpdate[] -} - -export type WebSocketEventInputText = WebSocketEventInputTextBase & Partial> - -export interface WebSocketEventInputTextVoiceBase { - transcription: string - textRaw?: string - overrides?: InputMessageOverrides - contextUpdates?: InputContextUpdate[] -} - -export type WebSocketEventInputTextVoice = WebSocketEventInputTextVoiceBase & Partial> - -export interface WebSocketEventInputVoiceBase { - audio: ArrayBuffer - overrides?: InputMessageOverrides - contextUpdates?: InputContextUpdate[] -} - -export type WebSocketEventInputVoice = WebSocketEventInputVoiceBase & Partial> - -export type WebSocketEventDataInputs = WebSocketEventInputText | WebSocketEventInputTextVoice | WebSocketEventInputVoice - -export type WebSocketEventInputs = WebSocketEventOf<'input:text'> | WebSocketEventOf<'input:text:voice'> | WebSocketEventOf<'input:voice'> +export * from '@proj-airi/plugin-protocol/types' export interface WebSocketEventBaseMetadata { source?: ModuleIdentity @@ -493,539 +27,12 @@ export interface WebSocketBaseEvent { route?: RouteConfig } -export type WithInputSource = { - [S in Source]: InputSource[S] -} +export interface WebSocketEvents extends ProtocolEvents {} -export type WithOutputSource = { - [S in Source]: OutputSource[S] -} - -// Module orchestration (local or remote transport): -// -// 1) module:authenticate → module:authenticated -// 2) registry:modules:sync (host → module bootstrap) -// 3) module:announce (identity, deps, config schema) -// 4) module:prepared -// 5) module:configuration:* (validate/plan/commit flow) -// 6) module:configuration:configured -// 7) module:contribute:capability:offer (repeat per capability) -// 8) module:contribute:capability:configuration:* (optional) -// 9) module:contribute:capability:activated -// 10) module:status (ready) -// 11) module:status:change (to re-run phases) - -interface ModuleAuthenticateEvent { - token: string -} - -interface ModuleAuthenticatedEvent { - authenticated: boolean -} - -interface RegistryModulesSyncEvent { - modules: Array<{ - name: string - index?: number - identity: ModuleIdentity - }> -} - -interface ErrorEvent { - message: string -} - -interface ModuleAnnounceEvent { - name: string - identity: ModuleIdentity - possibleEvents: Array<(keyof WebSocketEvents)> - configSchema?: ModuleConfigSchema - dependencies?: ModuleDependency[] -} - -interface ModulePreparedEvent { - identity: ModuleIdentity - missingDependencies?: ModuleDependency[] -} - -interface ModuleConfigurationNeededEvent { - identity: ModuleIdentity - schema?: ModuleConfigSchema - current?: ModuleConfigEnvelope - reason?: string -} - -interface ModuleStatusEvent { - identity: ModuleIdentity - phase: ModulePhase - reason?: string - details?: Record -} - -interface ModuleConfigurationValidateRequestEvent { - identity: ModuleIdentity - current?: ModuleConfigEnvelope -} - -interface ModuleConfigurationValidateResponseEvent { - identity: ModuleIdentity - validation: ModuleConfigValidation - plan?: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleConfigurationValidateStatusEvent { - identity: ModuleIdentity - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleConfigurationPlanRequestEvent { - identity: ModuleIdentity - plan?: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleConfigurationPlanResponseEvent { - identity: ModuleIdentity - plan: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleConfigurationPlanStatusEvent { - identity: ModuleIdentity - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleConfigurationCommitEvent { - identity: ModuleIdentity - config: ModuleConfigEnvelope -} - -interface ModuleConfigurationCommitStatusEvent { - identity: ModuleIdentity - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleConfigurationConfiguredEvent { - identity: ModuleIdentity - config: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityOfferEvent { - identity: ModuleIdentity - capability: ModuleCapability -} - -interface ModuleContributeCapabilityConfigurationNeededEvent { - identity: ModuleIdentity - capabilityId: string - schema?: ModuleConfigSchema - current?: ModuleConfigEnvelope - reason?: string -} - -interface ModuleContributeCapabilityConfigurationValidateRequestEvent { - identity: ModuleIdentity - capabilityId: string - current?: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityConfigurationValidateResponseEvent { - identity: ModuleIdentity - capabilityId: string - validation: ModuleConfigValidation - plan?: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityConfigurationValidateStatusEvent { - identity: ModuleIdentity - capabilityId: string - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleContributeCapabilityConfigurationPlanRequestEvent { - identity: ModuleIdentity - capabilityId: string - plan?: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityConfigurationPlanResponseEvent { - identity: ModuleIdentity - capabilityId: string - plan: ModuleConfigPlan - current?: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityConfigurationPlanStatusEvent { - identity: ModuleIdentity - capabilityId: string - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleContributeCapabilityConfigurationCommitEvent { - identity: ModuleIdentity - capabilityId: string - config: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityConfigurationCommitStatusEvent { - identity: ModuleIdentity - capabilityId: string - state: 'queued' | 'working' | 'done' | 'failed' - note?: string - progress?: number -} - -interface ModuleContributeCapabilityConfigurationConfiguredEvent { - identity: ModuleIdentity - capabilityId: string - config: ModuleConfigEnvelope -} - -interface ModuleContributeCapabilityActivatedEvent { - identity: ModuleIdentity - capabilityId: string - active: boolean - reason?: string -} - -interface ModuleStatusChangeEvent { - identity: ModuleIdentity - phase: ModulePhase - reason?: string - details?: Record -} - -interface ModuleConfigureEvent { - config: C | Record -} - -interface UiConfigureEvent { - moduleName: string - moduleIndex?: number - config: C | Record -} - -type OutputGenAiChatToolCallEvent = { - toolCalls: ToolMessage[] -} & Partial> & Partial> - -type OutputGenAiChatMessageEvent = { - message: AssistantMessage -} & Partial> & Partial> - -interface OutputGenAiChatUsage { - promptTokens: number - completionTokens: number - totalTokens: number - source: 'provider-based' | 'estimate-based' -} - -type OutputGenAiChatCompleteEvent = { - message: AssistantMessage - toolCalls: ToolMessage[] - usage: OutputGenAiChatUsage -} & Partial> & Partial> - -interface SparkNotifyEvent { - id: string - eventId: string - lane?: string - kind: 'alarm' | 'ping' | 'reminder' - urgency: 'immediate' | 'soon' | 'later' - headline: string - note?: string - payload?: Record - ttlMs?: number - requiresAck?: boolean - destinations: Array - metadata?: Record -} - -interface SparkEmitEvent { - id: string - eventId?: string - state: 'queued' | 'working' | 'done' | 'dropped' | 'blocked' | 'expired' - note?: string - destinations: Array - metadata?: Record -} - -interface SparkCommandGuidanceOption { - label: string - steps: Array - rationale?: string - possibleOutcome?: Array - risk?: 'high' | 'medium' | 'low' | 'none' - fallback?: Array - triggers?: Array -} - -interface SparkCommandGuidance { - type: 'proposal' | 'instruction' | 'memory-recall' - /** - * Personas can be used to adjust the behavior of sub-agents. - * For example, when using as NPC in games, or player in Minecraft, - * the persona can help define the character's traits and decision-making style. - * - * Example: - * persona: { - * "bravery": "high", - * "cautiousness": "low", - * "friendliness": "medium" - * } - */ - persona?: Record - options: Array -} - -interface SparkCommandEvent { - id: string - eventId?: string - parentEventId?: string - commandId: string - interrupt: 'force' | 'soft' | false - priority: 'critical' | 'high' | 'normal' | 'low' - intent: 'plan' | 'proposal' | 'action' | 'pause' | 'resume' | 'reroute' | 'context' - ack?: string - guidance?: SparkCommandGuidance - contexts?: Array - destinations: Array -} - -interface TransportConnectionHeartbeatEvent { - kind: MessageHeartbeatKind - message: MessageHeartbeat | string - at?: number -} - -type ContextUpdateEvent = ContextUpdate - -export const moduleAuthenticate = defineEventa('module:authenticate') -export const moduleAuthenticated = defineEventa('module:authenticated') -export const registryModulesSync = defineEventa('registry:modules:sync') - -export const error = defineEventa('error') - -export const moduleAnnounce = defineEventa('module:announce') -export const modulePrepared = defineEventa('module:prepared') -export const moduleConfigurationNeeded = defineEventa('module:configuration:needed') -export const moduleStatus = defineEventa('module:status') - -export const moduleConfigurationValidateRequest = defineEventa('module:configuration:validate:request') -export const moduleConfigurationValidateResponse = defineEventa('module:configuration:validate:response') -export const moduleConfigurationValidateStatus = defineEventa('module:configuration:validate:status') -export const moduleConfigurationPlanRequest = defineEventa('module:configuration:plan:request') -export const moduleConfigurationPlanResponse = defineEventa('module:configuration:plan:response') -export const moduleConfigurationPlanStatus = defineEventa('module:configuration:plan:status') -export const moduleConfigurationCommit = defineEventa('module:configuration:commit') -export const moduleConfigurationCommitStatus = defineEventa('module:configuration:commit:status') -export const moduleConfigurationConfigured = defineEventa('module:configuration:configured') - -export const moduleContributeCapabilityOffer = defineEventa('module:contribute:capability:offer') -export const moduleContributeCapabilityConfigurationNeeded = defineEventa('module:contribute:capability:configuration:needed') -export const moduleContributeCapabilityConfigurationValidateRequest = defineEventa('module:contribute:capability:configuration:validate:request') -export const moduleContributeCapabilityConfigurationValidateResponse = defineEventa('module:contribute:capability:configuration:validate:response') -export const moduleContributeCapabilityConfigurationValidateStatus = defineEventa('module:contribute:capability:configuration:validate:status') -export const moduleContributeCapabilityConfigurationPlanRequest = defineEventa('module:contribute:capability:configuration:plan:request') -export const moduleContributeCapabilityConfigurationPlanResponse = defineEventa('module:contribute:capability:configuration:plan:response') -export const moduleContributeCapabilityConfigurationPlanStatus = defineEventa('module:contribute:capability:configuration:plan:status') -export const moduleContributeCapabilityConfigurationCommit = defineEventa('module:contribute:capability:configuration:commit') -export const moduleContributeCapabilityConfigurationCommitStatus = defineEventa('module:contribute:capability:configuration:commit:status') -export const moduleContributeCapabilityConfigurationConfigured = defineEventa('module:contribute:capability:configuration:configured') -export const moduleContributeCapabilityActivated = defineEventa('module:contribute:capability:activated') - -export const moduleStatusChange = defineEventa('module:status:change') - -export const moduleConfigure = defineEventa('module:configure') - -export const uiConfigure = defineEventa('ui:configure') - -export const inputText = defineEventa('input:text') -export const inputTextVoice = defineEventa('input:text:voice') -export const inputVoice = defineEventa('input:voice') - -export const outputGenAiChatToolCall = defineEventa('output:gen-ai:chat:tool-call') -export const outputGenAiChatMessage = defineEventa('output:gen-ai:chat:message') -export const outputGenAiChatComplete = defineEventa('output:gen-ai:chat:complete') - -export const sparkNotify = defineEventa('spark:notify') -export const sparkEmit = defineEventa('spark:emit') -export const sparkCommand = defineEventa('spark:command') - -export const transportConnectionHeartbeat = defineEventa('transport:connection:heartbeat') -export const contextUpdate = defineEventa('context:update') - -// Thanks to: -// -// A little hack for creating extensible discriminated unions : r/typescript -// https://www.reddit.com/r/typescript/comments/1064ibt/a_little_hack_for_creating_extensible/ -export interface WebSocketEvents { - 'error': ErrorEvent - - 'module:authenticate': ModuleAuthenticateEvent - 'module:authenticated': ModuleAuthenticatedEvent - /** - * Server-side registry sync for known online modules. - * Sent to newly authenticated peers to bootstrap module discovery. - */ - 'registry:modules:sync': RegistryModulesSyncEvent - 'module:announce': ModuleAnnounceEvent - /** - * Prepare completed. Host can move into config apply/validate. - * - * Example: - * module:prepared { missingDependencies: [] } - */ - 'module:prepared': ModulePreparedEvent - /** - * Module needs configuration to proceed to prepared/configured. - */ - 'module:configuration:needed': ModuleConfigurationNeededEvent - /** - * Lifecycle status updates for orchestration/UX. - * - * Example: - * module:status { phase: "ready" } - */ - 'module:status': ModuleStatusEvent - /** - * Ask the module to validate current config (host → module). - */ - 'module:configuration:validate:request': ModuleConfigurationValidateRequestEvent - /** - * Validation response (module → host), with optional plan suggestions. - */ - 'module:configuration:validate:response': ModuleConfigurationValidateResponseEvent - /** - * Status updates for validation (module → host). - */ - 'module:configuration:validate:status': ModuleConfigurationValidateStatusEvent - /** - * Configuration planning request (host → module). - */ - 'module:configuration:plan:request': ModuleConfigurationPlanRequestEvent - /** - * Configuration planning response (module → host). - */ - 'module:configuration:plan:response': ModuleConfigurationPlanResponseEvent - /** - * Status updates for planning (module → host). - */ - 'module:configuration:plan:status': ModuleConfigurationPlanStatusEvent - /** - * Commit a config as "active" (host → module). - */ - 'module:configuration:commit': ModuleConfigurationCommitEvent - /** - * Status updates for commit (module → host). - */ - 'module:configuration:commit:status': ModuleConfigurationCommitStatusEvent - /** - * Configuration fully applied and active (module → host). - */ - 'module:configuration:configured': ModuleConfigurationConfiguredEvent - /** - * Capability offer emitted after module configuration. - */ - 'module:contribute:capability:offer': ModuleContributeCapabilityOfferEvent - /** - * Capability needs configuration before activation. - */ - 'module:contribute:capability:configuration:needed': ModuleContributeCapabilityConfigurationNeededEvent - 'module:contribute:capability:configuration:validate:request': ModuleContributeCapabilityConfigurationValidateRequestEvent - 'module:contribute:capability:configuration:validate:response': ModuleContributeCapabilityConfigurationValidateResponseEvent - 'module:contribute:capability:configuration:validate:status': ModuleContributeCapabilityConfigurationValidateStatusEvent - 'module:contribute:capability:configuration:plan:request': ModuleContributeCapabilityConfigurationPlanRequestEvent - 'module:contribute:capability:configuration:plan:response': ModuleContributeCapabilityConfigurationPlanResponseEvent - 'module:contribute:capability:configuration:plan:status': ModuleContributeCapabilityConfigurationPlanStatusEvent - 'module:contribute:capability:configuration:commit': ModuleContributeCapabilityConfigurationCommitEvent - 'module:contribute:capability:configuration:commit:status': ModuleContributeCapabilityConfigurationCommitStatusEvent - 'module:contribute:capability:configuration:configured': ModuleContributeCapabilityConfigurationConfiguredEvent - 'module:contribute:capability:activated': ModuleContributeCapabilityActivatedEvent - /** - * Request a phase transition (module → host). - */ - 'module:status:change': ModuleStatusChangeEvent - /** - * Push configuration down to module (host → module). - */ - 'module:configure': ModuleConfigureEvent - - 'ui:configure': UiConfigureEvent - - 'input:text': WebSocketEventInputText - 'input:text:voice': WebSocketEventInputTextVoice - 'input:voice': WebSocketEventInputVoice - - 'output:gen-ai:chat:tool-call': OutputGenAiChatToolCallEvent - 'output:gen-ai:chat:message': OutputGenAiChatMessageEvent - 'output:gen-ai:chat:complete': OutputGenAiChatCompleteEvent - - /** - * Spark used for allowing agents in a network to raise an event toward the other destinations (e.g. character). - * - * DO: - * - Use notify for episodic events (alarms/pings/reminders) with minimal payload. - * - Use command for high-level intent; let sub-agents translate into their own state machines. - * - Use emit for ack/progress/completion; include ids for tracing/dedupe. - * - Route via destinations; keep payloads small; use context:update for richer ideas. - * - Dedupe/log via id/eventId for observability. - * - * DOn't: - * - Stream high-frequency telemetry here (keep a separate channel). - * - Stuff large blobs into payload/contexts; prefer refs/summaries. - * - Assume exactly-once; add retry/ack on critical paths. You may rely on id/eventId for dedupe. - * - Allow untrusted agents to broadcast without auth/capability checks. - * - * Examples: - * - Minecraft attack/death: kind=alarm, urgency=immediate (fast bubble-up). - * e.g., fromAgent='minecraft', headline='Under attack by witch', payload includes hp/location/gear. - * - Cat bowl empty from HomeAssistant: kind=alarm, urgency=soon. - * - IM/email "read now": kind=ping, urgency=immediate. - * - Action Required email: kind=reminder, urgency=later. - * - * destinations controls routing (e.g. ['character'], ['character','minecraft-agent']). - */ - 'spark:notify': SparkNotifyEvent - - /** - * Acknowledgement/progress/state for a spark or command (bidirectional). - * Examples: - * - Character: state=working, note="Seen it, responding". - * - Sub-agent: state=done, note="Healed and safe". - * - Sub-agent: state=blocked/dropped with note when it cannot comply. - * - Minecraft: state=working, note="Pillared up; healing" in reply to a command. - */ - 'spark:emit': SparkEmitEvent - - /** - * Character issues instructions or context to a sub-agent. - * interrupt: force = hard preempt; soft = merge/queue. - * Examples: - * - Witch attack: interrupt=force, priority=critical, intent=action with options (aggressive/cautious). - * e.g., options to block/retreat vs push with shield/sword, with fallback steps. - * - Prep plan: interrupt=soft, priority=high, intent=plan with steps/fallbacks. - * - Contextual hints: intent=context with contextPatch ideas/hints. - */ - 'spark:command': SparkCommandEvent - - 'transport:connection:heartbeat': TransportConnectionHeartbeatEvent - - 'context:update': ContextUpdateEvent -} +export type WebSocketEventDataInputs + = | WebSocketEvents['input:text'] + | WebSocketEvents['input:text:voice'] + | WebSocketEvents['input:voice'] export type WebSocketEvent = { [K in keyof WebSocketEvents]: WebSocketBaseEvent[K]>; @@ -1038,3 +45,8 @@ export type WebSocketEventOptionalSource = { export type WebSocketEventOf = E extends keyof WebSocketEvents ? Omit[E]>, 'metadata'> & { metadata?: WebSocketEventBaseMetadata } : never + +export type WebSocketEventInputs + = | WebSocketEventOf<'input:text'> + | WebSocketEventOf<'input:text:voice'> + | WebSocketEventOf<'input:voice'> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82bde980b..09bd7aede 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1970,6 +1970,15 @@ importers: specifier: ^1.0.2 version: 1.0.2 + packages/plugin-protocol: + dependencies: + '@moeru/eventa': + specifier: 'catalog:' + version: 1.0.0-alpha.14(electron@40.0.0) + '@xsai/shared-chat': + specifier: 'catalog:' + version: 0.4.0-beta.13 + packages/plugin-sdk: dependencies: '@moeru/eventa': @@ -2021,12 +2030,9 @@ importers: packages/server-shared: dependencies: - '@moeru/eventa': - specifier: 'catalog:' - version: 1.0.0-alpha.14(electron@40.0.0) - '@xsai/shared-chat': - specifier: 'catalog:' - version: 0.4.0-beta.13 + '@proj-airi/plugin-protocol': + specifier: workspace:* + version: link:../plugin-protocol packages/stage-layouts: dependencies: