feat(stage-ui,airi-plugin-vscode,server-shared): added spark:notify, spark:emit, spark:command events

This commit is contained in:
Neko Ayaka
2025-12-27 21:20:06 +08:00
parent f07964d307
commit ca7194284b
7 changed files with 129 additions and 19 deletions
@@ -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<string>
hints?: Array<string>
strategy: ContextUpdateStrategy
text: string
content?: Content
@@ -132,6 +141,93 @@ export interface WebSocketEvents<C = undefined> {
}
} & Partial<WithInputSource<'stage-web' | 'stage-tamagotchi' | 'discord'>> & Partial<WithOutputSource<'gen-ai:chat'>>
/**
* 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<string, unknown>
ttlMs?: number
requiresAck?: boolean
destinations: Array<string>
}
/**
* 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<string>
}
/**
* 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<string, 'very-high' | 'high' | 'medium' | 'low' | 'very-low'>
options: Array<{
label: string
steps: Array<string>
rationale?: string
possibleOutcome?: Array<string>
risk?: 'high' | 'medium' | 'low' | 'none'
fallback?: Array<string>
triggers?: Array<string>
}>
}
contexts?: Array<ContextUpdate>
destinations: Array<string>
}
'context:update': ContextUpdate
}
@@ -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<ContextUpdate, 'id' | 'contextId'> & Partial<Pick<ContextUpdate, 'id' | 'contextId'>>) {
const id = nanoid()
send({ type: 'context:update', data: { id, contextId: id, ...message } })
}
function dispose() {
@@ -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: {
+2 -1
View File
@@ -68,7 +68,8 @@
},
"dependencies": {
"es-toolkit": "catalog:",
"injeca": "catalog:"
"injeca": "catalog:",
"nanoid": "catalog:"
},
"devDependencies": {
"@guiiai/logg": "catalog:",
+5 -2
View File
@@ -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<Events> | null = null
@@ -45,11 +46,13 @@ export class Client {
}
async replaceContext(context: string): Promise<void> {
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<void> {
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 {
+6
View File
@@ -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:'
+15 -13
View File
@@ -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