diff --git a/packages/server-shared/src/types/websocket/events.ts b/packages/server-shared/src/types/websocket/events.ts index bcf69405c..33193abb2 100644 --- a/packages/server-shared/src/types/websocket/events.ts +++ b/packages/server-shared/src/types/websocket/events.ts @@ -55,6 +55,15 @@ export interface ContextUpdate< // 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 @@ -132,6 +141,93 @@ export interface WebSocketEvents { } } & Partial> & Partial> + /** + * Sub-agent raises an event toward the character (or other destinations). + * 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': { + 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 + } + + /** + * 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': { + id: string + eventId?: string + state: 'queued' | 'working' | 'done' | 'dropped' | 'blocked' | 'expired' + note?: string + destinations: Array + } + + /** + * 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': { + id: string + eventId?: string + parentEventId?: string + commandId: string + interrupt: 'force' | 'soft' | false + priority: 'critical' | 'high' | 'normal' | 'low' + intent: 'action' | 'plan' | 'pause' | 'resume' | 'reroute' | 'context' + ack?: string + guidance?: { + 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<{ + label: string + steps: Array + rationale?: string + possibleOutcome?: Array + risk?: 'high' | 'medium' | 'low' | 'none' + fallback?: Array + triggers?: Array + }> + } + contexts?: Array + destinations: Array + } + 'context:update': ContextUpdate } diff --git a/packages/stage-ui/src/stores/mods/api/channel-server.ts b/packages/stage-ui/src/stores/mods/api/channel-server.ts index d7653c5e9..93c8bc9dd 100644 --- a/packages/stage-ui/src/stores/mods/api/channel-server.ts +++ b/packages/stage-ui/src/stores/mods/api/channel-server.ts @@ -2,6 +2,7 @@ import type { ContextUpdate, WebSocketBaseEvent, WebSocketEvent, WebSocketEventO import { Client, WebSocketEventSource } from '@proj-airi/server-sdk' import { isStageTamagotchi, isStageWeb } from '@proj-airi/stage-shared' +import { nanoid } from 'nanoid' import { defineStore } from 'pinia' import { ref } from 'vue' @@ -97,8 +98,9 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se } } - function sendContextUpdate(message: ContextUpdate) { - send({ type: 'context:update', data: message }) + function sendContextUpdate(message: Omit & Partial>) { + const id = nanoid() + send({ type: 'context:update', data: { id, contextId: id, ...message } }) } function dispose() { diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.ts index e2c7f2a05..c6fb96d72 100644 --- a/packages/stage-ui/src/stores/mods/api/context-bridge.ts +++ b/packages/stage-ui/src/stores/mods/api/context-bridge.ts @@ -89,7 +89,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () = broadcastStreamEvent({ type: 'assistant-end', message, sessionId: chatStore.activeSessionId, context }) }), - chatStore.onAssistantMessage(async (message, messageText, context) => { + chatStore.onAssistantMessage(async (message, _messageText, context) => { serverChannelStore.send({ type: 'output:gen-ai:chat:message', data: { diff --git a/plugins/airi-plugin-vscode/package.json b/plugins/airi-plugin-vscode/package.json index 0acb4a661..7af49a3e1 100644 --- a/plugins/airi-plugin-vscode/package.json +++ b/plugins/airi-plugin-vscode/package.json @@ -68,7 +68,8 @@ }, "dependencies": { "es-toolkit": "catalog:", - "injeca": "catalog:" + "injeca": "catalog:", + "nanoid": "catalog:" }, "devDependencies": { "@guiiai/logg": "catalog:", diff --git a/plugins/airi-plugin-vscode/src/airi.ts b/plugins/airi-plugin-vscode/src/airi.ts index 3eb5ad29b..d606eb500 100644 --- a/plugins/airi-plugin-vscode/src/airi.ts +++ b/plugins/airi-plugin-vscode/src/airi.ts @@ -4,6 +4,7 @@ import type { Events } from './types' import { useLogger } from '@guiiai/logg' import { ContextUpdateStrategy, Client as ServerClient } from '@proj-airi/server-sdk' +import { nanoid } from 'nanoid' export class Client { private client: ServerClient | null = null @@ -45,11 +46,13 @@ export class Client { } async replaceContext(context: string): Promise { - this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.ReplaceSelf, text: context } }) + const id = nanoid() + this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.ReplaceSelf, text: context, id, contextId: id } }) } async appendContext(context: string): Promise { - this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.AppendSelf, text: context } }) + const id = nanoid() + this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.AppendSelf, text: context, id, contextId: id } }) } isConnected(): boolean { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index debde7600..3bf156f21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ catalogs: injeca: specifier: ^0.1.5 version: 0.1.5 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 posthog-js: specifier: 1.306.1 version: 1.306.1 @@ -2089,6 +2092,9 @@ importers: injeca: specifier: 'catalog:' version: 0.1.5(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) + nanoid: + specifier: 'catalog:' + version: 5.1.6 devDependencies: '@guiiai/logg': specifier: 'catalog:' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7a3ab11df..b8905fef2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,3 @@ -catalogMode: prefer - -shellEmulator: true packages: - packages/** - plugins/** @@ -10,16 +7,6 @@ packages: - apps/** - '!**/dist/**' -overrides: - array-flatten: npm:@nolyfill/array-flatten@^1.0.44 - axios: npm:feaxios@^0.0.23 - is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - isarray: npm:@nolyfill/isarray@^1.0.44 - safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44 - safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 - side-channel: npm:@nolyfill/side-channel@^1.0.44 - string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 - catalog: '@guiiai/logg': ^1.2.11 '@moeru/eslint-config': 0.1.0-beta.14 @@ -43,11 +30,14 @@ catalog: embla-carousel-vue: ^8.6.0 es-toolkit: 1.43.0 injeca: ^0.1.5 + nanoid: ^5.1.6 posthog-js: 1.306.1 uncrypto: ^0.1.3 unplugin-info: 1.2.4 xsschema: ^0.4.0-beta.12 +catalogMode: prefer + catalogs: rolldown-vite: vite: npm:rolldown-vite@^7.3.0 @@ -76,3 +66,15 @@ onlyBuiltDependencies: - spawn-sync - utf-8-validate - vue-demi + +overrides: + array-flatten: npm:@nolyfill/array-flatten@^1.0.44 + axios: npm:feaxios@^0.0.23 + is-core-module: npm:@nolyfill/is-core-module@^1.0.39 + isarray: npm:@nolyfill/isarray@^1.0.44 + safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44 + safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 + side-channel: npm:@nolyfill/side-channel@^1.0.44 + string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 + +shellEmulator: true