diff --git a/apps/stage-pocket/src/App.vue b/apps/stage-pocket/src/App.vue
index 7c0ae4825..43e4c642b 100644
--- a/apps/stage-pocket/src/App.vue
+++ b/apps/stage-pocket/src/App.vue
@@ -1,7 +1,7 @@
@@ -42,7 +79,7 @@ const {
{{ getEventSource(entry) }}
diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-preview.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-preview.vue
index 0f917c567..012992b66 100644
--- a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-preview.vue
+++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-preview.vue
@@ -19,11 +19,8 @@ const { buildPreviewItems } = useContextFlowFormatters()
:key="`${entry.id}-${item.label}`"
:class="[
'rounded-lg',
- 'border',
- 'border-neutral-200/70',
'bg-white/80',
'p-3',
- 'dark:border-neutral-800/80',
'dark:bg-neutral-900/70',
]"
>
diff --git a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-spark-notify.vue b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-spark-notify.vue
index 224866b95..e1fa40d84 100644
--- a/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-spark-notify.vue
+++ b/packages/stage-pages/src/pages/devtools/context-flow/components/context-flow-spark-notify.vue
@@ -13,12 +13,9 @@ const { buildSparkCommandPreview } = useContextFlowFormatters()
:class="[
'mt-3',
'rounded-lg',
- 'border',
- 'border-neutral-200/70',
'bg-white/80',
'p-3',
'text-xs',
- 'dark:border-neutral-800/80',
'dark:bg-neutral-900/70',
'grid',
'gap-3',
@@ -29,7 +26,7 @@ const { buildSparkCommandPreview } = useContextFlowFormatters()
{{ state.handling ? 'Handling spark:notify...' : 'spark:notify handled' }}
diff --git a/packages/stage-pages/src/pages/devtools/context-flow/composables/use-context-flow-formatters.ts b/packages/stage-pages/src/pages/devtools/context-flow/composables/use-context-flow-formatters.ts
index 7c8860e0f..7d97e7574 100644
--- a/packages/stage-pages/src/pages/devtools/context-flow/composables/use-context-flow-formatters.ts
+++ b/packages/stage-pages/src/pages/devtools/context-flow/composables/use-context-flow-formatters.ts
@@ -1,6 +1,6 @@
import type { WebSocketEvents } from '@proj-airi/server-sdk'
-import type { FlowChannel, FlowDirection, FlowEntry, PreviewItem } from '../context-flow-types'
+import type { FlowEntry, PreviewItem } from '../context-flow-types'
const previewMaxLength = 420
@@ -187,57 +187,15 @@ function formatPayload(payload: unknown) {
}
}
-function directionBadgeClasses(direction: FlowDirection) {
- if (direction === 'incoming') {
- return [
- 'bg-complementary-500/15',
- 'text-complementary-600',
- 'dark:text-complementary-300',
- 'border-complementary-500/30',
- ]
- }
- return [
- 'bg-primary-500/15',
- 'text-primary-600',
- 'dark:text-primary-300',
- 'border-primary-500/30',
- ]
-}
-
-function directionIconClass(direction: FlowDirection) {
- return direction === 'incoming' ? 'i-solar:arrow-down-linear' : 'i-solar:arrow-up-linear'
-}
-
-function channelBadgeClasses(channel: FlowChannel) {
- switch (channel) {
- case 'server':
- return ['bg-orange-500/15', 'text-orange-600', 'dark:text-orange-300', 'border-orange-500/30']
- case 'broadcast':
- return ['bg-violet-500/15', 'text-violet-600', 'dark:text-violet-300', 'border-violet-500/30']
- case 'chat':
- return ['bg-lime-500/15', 'text-lime-600', 'dark:text-lime-300', 'border-lime-500/30']
- default:
- return ['bg-neutral-400/15', 'text-neutral-600', 'dark:text-neutral-300', 'border-neutral-500/30']
- }
-}
-
-function sourceBadgeClasses() {
- return ['bg-neutral-400/15', 'text-neutral-600', 'dark:text-neutral-300', 'border-neutral-500/30']
-}
-
export function useContextFlowFormatters() {
return {
buildPreviewItems,
buildSparkCommandPreview,
- channelBadgeClasses,
- directionBadgeClasses,
- directionIconClass,
formatDestinations,
formatPayload,
formatTimestamp,
getEventSource,
getPayloadData,
- sourceBadgeClasses,
summarizeContextUpdate,
truncateText,
}
diff --git a/packages/stage-pages/src/pages/devtools/context-flow/index.vue b/packages/stage-pages/src/pages/devtools/context-flow/index.vue
index 99057edc1..e86bd1a2e 100644
--- a/packages/stage-pages/src/pages/devtools/context-flow/index.vue
+++ b/packages/stage-pages/src/pages/devtools/context-flow/index.vue
@@ -6,8 +6,7 @@ import type { FlowDirection, FlowEntry, SparkNotifyEntryState } from './context-
import { errorMessageFrom } from '@moeru/std'
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
-import { useCharacterStore } from '@proj-airi/stage-ui/stores/character'
-import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character-orchestrator'
+import { useCharacterOrchestratorStore, useCharacterStore } from '@proj-airi/stage-ui/stores/character'
import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME, useChatStore } from '@proj-airi/stage-ui/stores/chat'
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
import { Callout } from '@proj-airi/ui'
diff --git a/packages/stage-pages/tsconfig.json b/packages/stage-pages/tsconfig.json
index e66da7a63..ed02e9ad2 100644
--- a/packages/stage-pages/tsconfig.json
+++ b/packages/stage-pages/tsconfig.json
@@ -9,6 +9,11 @@
],
"module": "ESNext",
"moduleResolution": "Bundler",
+ "paths": {
+ "@proj-airi/stage-ui/*": [
+ "../../packages/stage-ui/src/*"
+ ]
+ },
"resolveJsonModule": true,
"types": [
"vitest",
diff --git a/packages/stage-ui/package.json b/packages/stage-ui/package.json
index 005fea1da..2969dfc29 100644
--- a/packages/stage-ui/package.json
+++ b/packages/stage-ui/package.json
@@ -28,6 +28,7 @@
"./libs": "./src/libs/index.ts",
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
"./stores/analytics": "./src/stores/analytics/index.ts",
+ "./stores/character": "./src/stores/character/index.ts",
"./stores/*": "./src/stores/*.ts",
"./stores": "./src/stores/index.ts",
"./workers/vad": "./src/workers/vad/index.ts",
diff --git a/packages/stage-ui/src/stores/character.ts b/packages/stage-ui/src/stores/character/index.ts
similarity index 91%
rename from packages/stage-ui/src/stores/character.ts
rename to packages/stage-ui/src/stores/character/index.ts
index cce1e345f..8984fe555 100644
--- a/packages/stage-ui/src/stores/character.ts
+++ b/packages/stage-ui/src/stores/character/index.ts
@@ -1,12 +1,14 @@
-import type { TextSegmentationItem } from '../composables/queues'
+import type { TextSegmentationItem } from '../../composables/queues'
import { nanoid } from 'nanoid'
import { defineStore, storeToRefs } from 'pinia'
import { computed, reactive, ref } from 'vue'
-import { usePipelineWorkflowTextSegmentationStore } from '../composables/queues'
-import { TTS_FLUSH_INSTRUCTION } from '../utils/tts'
-import { useAiriCardStore } from './modules'
+import { usePipelineWorkflowTextSegmentationStore } from '../../composables/queues'
+import { TTS_FLUSH_INSTRUCTION } from '../../utils/tts'
+import { useAiriCardStore } from '../modules'
+
+export * from './orchestrator'
export interface CharacterSparkNotifyReaction {
id: string
diff --git a/packages/stage-ui/src/stores/character-orchestrator.ts b/packages/stage-ui/src/stores/character/orchestrator/agents/event-handler-spark-notify/index.ts
similarity index 72%
rename from packages/stage-ui/src/stores/character-orchestrator.ts
rename to packages/stage-ui/src/stores/character/orchestrator/agents/event-handler-spark-notify/index.ts
index a06ee3c53..44e944bd8 100644
--- a/packages/stage-ui/src/stores/character-orchestrator.ts
+++ b/packages/stage-ui/src/stores/character/orchestrator/agents/event-handler-spark-notify/index.ts
@@ -1,24 +1,16 @@
import type { WebSocketBaseEvent, WebSocketEvents } from '@proj-airi/server-sdk'
-import type { ChatProvider } from '@xsai-ext/providers/utils'
+import type { ChatProvider, ChatProviderWithExtraOptions, EmbedProvider, EmbedProviderWithExtraOptions, SpeechProvider, SpeechProviderWithExtraOptions, TranscriptionProvider, TranscriptionProviderWithExtraOptions } from '@xsai-ext/providers/utils'
import type { Message } from '@xsai/shared-chat'
-import type { StreamEvent } from './llm'
+import type { StreamEvent } from '../../../../llm'
import { errorMessageFrom } from '@moeru/std'
import { tool } from '@xsai/tool'
import { nanoid } from 'nanoid'
-import { defineStore, storeToRefs } from 'pinia'
-import { ref } from 'vue'
import { validate } from 'xsschema'
import { z } from 'zod'
-import { useCharacterStore } from './character'
-import { useLLM } from './llm'
-import { useModsServerChannelStore } from './mods/api/channel-server'
-import { useConsciousnessStore } from './modules/consciousness'
-import { useProvidersStore } from './providers'
-
-interface SparkNotifyCommandDraft {
+export interface SparkNotifyCommandDraft {
destinations: string[]
interrupt?: 'force' | 'soft' | boolean
priority?: 'critical' | 'high' | 'normal' | 'low'
@@ -28,11 +20,45 @@ interface SparkNotifyCommandDraft {
contexts?: WebSocketEvents['spark:command']['contexts']
}
-interface SparkNotifyResponse {
+export interface SparkNotifyResponse {
reaction?: string
commands?: SparkNotifyCommandDraft[]
}
+export interface SparkNotifyAgentDeps {
+ stream: (
+ model: string,
+ provider: ChatProvider,
+ messages: Message[],
+ options: {
+ tools?: any[]
+ supportsTools?: boolean
+ waitForTools?: boolean
+ onStreamEvent?: (event: StreamEvent) => void | Promise
+ },
+ ) => Promise
+ getActiveProvider: () => string | undefined
+ getActiveModel: () => string | undefined
+ getProviderInstance: (name: string,
+ ) => Promise
+ onReactionDelta: (eventId: string, text: string) => void
+ onReactionEnd: (eventId: string, text: string) => void
+ getSystemPrompt: () => string
+ getProcessing: () => boolean
+ setProcessing: (next: boolean) => void
+ getPending: () => Array>
+ setPending: (next: Array>) => void
+}
+
function getSparkNotifyHandlingAgentInstruction(moduleName: string) {
return [
'This is AIRI system, the life pod hosting your consciousness. You don\'t need to respond to me or every spark:notify event directly.',
@@ -72,33 +98,25 @@ export const sparkCommandSchema = z.object({
export type SparkCommandSchema = z.infer
-export const useCharacterOrchestratorStore = defineStore('character-orchestrator', () => {
- const { stream } = useLLM()
- const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
- const providersStore = useProvidersStore()
- const characterStore = useCharacterStore()
- const { systemPrompt } = storeToRefs(characterStore)
- const modsServerChannelStore = useModsServerChannelStore()
-
- const processing = ref(false)
- const pendingNotifies = ref>>([])
-
+export function setupAgentSparkNotifyHandler(deps: SparkNotifyAgentDeps) {
async function runNotifyAgent(event: WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']>) {
- if (!activeProvider.value || !activeModel.value) {
+ const activeProvider = deps.getActiveProvider()
+ const activeModel = deps.getActiveModel()
+ if (!activeProvider || !activeModel) {
console.warn('Spark notify ignored: missing active provider or model')
return undefined
}
- const chatProvider = await providersStore.getProviderInstance(activeProvider.value)
+ const chatProvider = await deps.getProviderInstance(activeProvider)
const commandDrafts: SparkNotifyCommandDraft[] = []
let noResponse = false
const sparkNoResponseTool = await tool({
name: 'builtIn_sparkNoResponse',
- description: `Indicate that no response or action is needed for the current spark:notify event.`,
+ description: 'Indicate that no response or action is needed for the current spark:notify event.',
parameters: z.object({}).strict(),
- execute: async (_payload) => {
+ execute: async () => {
noResponse = true
return 'AIRI System: Acknowledged, no response or action will be processed.'
},
@@ -106,7 +124,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
const sparkCommandTool = await tool({
name: 'builtIn_sparkCommand',
- description: `Issue a spark:command to sub-agents. You can call this tool multiple times to issue matrices of commands to different sub-agents as needed.`,
+ description: 'Issue a spark:command to sub-agents. You can call this tool multiple times to issue matrices of commands to different sub-agents as needed.',
parameters: sparkCommandSchema,
execute: async (payload) => {
try {
@@ -153,7 +171,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
const systemMessage: Message = {
role: 'system',
content: [
- systemPrompt.value,
+ deps.getSystemPrompt(),
getSparkNotifyHandlingAgentInstruction(event.source),
].filter(Boolean).join('\n\n'),
}
@@ -168,32 +186,32 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
let fullText = ''
- await stream(activeModel.value, chatProvider, [systemMessage, userMessage], {
+ await deps.stream(activeModel, chatProvider, [systemMessage, userMessage], {
tools: [
sparkNoResponseTool,
sparkCommandTool,
],
- supportsTools: true, // we expect tools to be supported
- waitForTools: true, // see https://github.com/moeru-ai/airi/issues/907
+ supportsTools: true,
+ waitForTools: true,
onStreamEvent: async (streamEvent: StreamEvent) => {
if (streamEvent.type === 'text-delta') {
if (noResponse)
return
- characterStore.onSparkNotifyReactionStreamEvent(event.data.id, streamEvent.text)
+ deps.onReactionDelta(event.data.id, streamEvent.text)
fullText += streamEvent.text
}
if (streamEvent.type === 'finish') {
if (noResponse) {
- characterStore.onSparkNotifyReactionStreamEnd(event.data.id, '')
+ deps.onReactionEnd(event.data.id, '')
return
}
- characterStore.onSparkNotifyReactionStreamEnd(event.data.id, fullText)
+ deps.onReactionEnd(event.data.id, fullText)
}
if (streamEvent.type === 'error') {
- characterStore.onSparkNotifyReactionStreamEnd(event.data.id, fullText)
+ deps.onReactionEnd(event.data.id, fullText)
throw streamEvent.error ?? new Error('Spark notify stream error')
}
},
@@ -205,17 +223,17 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
} satisfies SparkNotifyResponse
}
- async function handleSparkNotify(event: WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']>) {
- if (event.data.urgency !== 'immediate' && pendingNotifies.value.length > 0) {
- pendingNotifies.value = [...pendingNotifies.value, event]
+ async function handle(event: WebSocketBaseEvent<'spark:notify', WebSocketEvents['spark:notify']>) {
+ if (event.data.urgency !== 'immediate' && deps.getPending().length > 0) {
+ deps.setPending([...deps.getPending(), event])
return undefined
}
- if (processing.value) {
- pendingNotifies.value = [...pendingNotifies.value, event]
+ if (deps.getProcessing()) {
+ deps.setPending([...deps.getPending(), event])
return undefined
}
- processing.value = true
+ deps.setProcessing(true)
try {
const response = await runNotifyAgent(event)
@@ -243,51 +261,11 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
}
}
finally {
- processing.value = false
+ deps.setProcessing(false)
}
}
- async function handleSparkEmit(_: WebSocketBaseEvent<'spark:emit', WebSocketEvents['spark:emit']>) {
- // Currently no-op
- return undefined
- }
-
- function initialize() {
- modsServerChannelStore.onEvent('spark:notify', async (event) => {
- try {
- const result = await handleSparkNotify(event)
- if (!result?.commands?.length)
- return
-
- for (const command of result.commands) {
- modsServerChannelStore.send({
- type: 'spark:command',
- data: command,
- })
- }
- }
- catch (error) {
- console.warn('Failed to handle spark:notify event:', error)
- }
- })
-
- modsServerChannelStore.onEvent('spark:emit', async (event) => {
- try {
- await handleSparkEmit(event)
- }
- catch (error) {
- console.warn('Failed to handle spark:emit event:', error)
- }
- })
- }
-
return {
- processing,
- pendingNotifies,
-
- initialize,
-
- handleSparkNotify,
- handleSparkEmit,
+ handle,
}
-})
+}
diff --git a/packages/stage-ui/src/stores/character-orchestrator.test.ts b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts
similarity index 95%
rename from packages/stage-ui/src/stores/character-orchestrator.test.ts
rename to packages/stage-ui/src/stores/character/orchestrator/index.test.ts
index 7859e9c76..3ace3f510 100644
--- a/packages/stage-ui/src/stores/character-orchestrator.test.ts
+++ b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts
@@ -7,8 +7,8 @@ import type { Mock } from 'vitest'
import type { UnwrapRef } from 'vue'
import type z from 'zod'
-import type { StreamEvent } from './llm'
-import type { AiriCard } from './modules'
+import type { StreamEvent } from '../../llm'
+import type { AiriCard } from '../../modules'
import { createTestingPinia } from '@pinia/testing'
import { tool } from '@xsai/tool'
@@ -16,11 +16,11 @@ import { nanoid } from 'nanoid'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { useCharacterStore } from './character'
-import { sparkCommandSchema, useCharacterOrchestratorStore } from './character-orchestrator'
-import { useLLM } from './llm'
-import { useAiriCardStore, useConsciousnessStore } from './modules'
-import { useProvidersStore } from './providers'
+import { sparkCommandSchema, useCharacterOrchestratorStore } from '.'
+import { useCharacterStore } from '..'
+import { useLLM } from '../../llm'
+import { useAiriCardStore, useConsciousnessStore } from '../../modules'
+import { useProvidersStore } from '../../providers'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
diff --git a/packages/stage-ui/src/stores/character/orchestrator/index.ts b/packages/stage-ui/src/stores/character/orchestrator/index.ts
new file mode 100644
index 000000000..16c863321
--- /dev/null
+++ b/packages/stage-ui/src/stores/character/orchestrator/index.ts
@@ -0,0 +1 @@
+export * from './store'
diff --git a/packages/stage-ui/src/stores/character/orchestrator/store.ts b/packages/stage-ui/src/stores/character/orchestrator/store.ts
new file mode 100644
index 000000000..8cbc8d83c
--- /dev/null
+++ b/packages/stage-ui/src/stores/character/orchestrator/store.ts
@@ -0,0 +1,82 @@
+import type { WebSocketBaseEvent, WebSocketEvents } from '@proj-airi/server-sdk'
+
+import { defineStore, storeToRefs } from 'pinia'
+import { ref } from 'vue'
+
+import { useCharacterStore } from '../'
+import { useLLM } from '../../llm'
+import { useModsServerChannelStore } from '../../mods/api/channel-server'
+import { useConsciousnessStore } from '../../modules/consciousness'
+import { useProvidersStore } from '../../providers'
+import { setupAgentSparkNotifyHandler } from './agents/event-handler-spark-notify'
+
+export { sparkCommandSchema } from './agents/event-handler-spark-notify'
+
+export const useCharacterOrchestratorStore = defineStore('character-orchestrator', () => {
+ const { stream } = useLLM()
+ const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
+ const providersStore = useProvidersStore()
+ const characterStore = useCharacterStore()
+ const { systemPrompt } = storeToRefs(characterStore)
+ const modsServerChannelStore = useModsServerChannelStore()
+
+ const processing = ref(false)
+ const pendingNotifies = ref>>([])
+ const sparkNotifyAgent = setupAgentSparkNotifyHandler({
+ stream,
+ getActiveProvider: () => activeProvider.value,
+ getActiveModel: () => activeModel.value,
+ getProviderInstance: name => providersStore.getProviderInstance(name),
+ onReactionDelta: (eventId, text) => characterStore.onSparkNotifyReactionStreamEvent(eventId, text),
+ onReactionEnd: (eventId, text) => characterStore.onSparkNotifyReactionStreamEnd(eventId, text),
+ getSystemPrompt: () => systemPrompt.value,
+ getProcessing: () => processing.value,
+ setProcessing: next => processing.value = next,
+ getPending: () => pendingNotifies.value,
+ setPending: next => pendingNotifies.value = next,
+ })
+
+ async function handleSparkEmit(_: WebSocketBaseEvent<'spark:emit', WebSocketEvents['spark:emit']>) {
+ // Currently no-op
+ return undefined
+ }
+
+ function initialize() {
+ modsServerChannelStore.onEvent('spark:notify', async (event) => {
+ try {
+ const result = await sparkNotifyAgent.handle(event)
+ if (!result?.commands?.length)
+ return
+
+ for (const command of result.commands) {
+ modsServerChannelStore.send({
+ type: 'spark:command',
+ data: command,
+ })
+ }
+ }
+ catch (error) {
+ console.warn('Failed to handle spark:notify event:', error)
+ }
+ })
+
+ modsServerChannelStore.onEvent('spark:emit', async (event) => {
+ try {
+ await handleSparkEmit(event)
+ }
+ catch (error) {
+ console.warn('Failed to handle spark:emit event:', error)
+ }
+ })
+ }
+
+ return {
+ processing,
+ pendingNotifies,
+
+ initialize,
+
+ handleSparkNotify: sparkNotifyAgent.handle,
+ handleSparkEmit,
+ }
+})