From 0cde81d2a93f17d56359de0cba5f1d4839ed4028 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Wed, 7 Jan 2026 00:49:20 +0800 Subject: [PATCH] fix(stage-ui): character orchestrator should have prompts --- .../src/stores/character-orchestrator.ts | 117 +++++++++++++----- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/packages/stage-ui/src/stores/character-orchestrator.ts b/packages/stage-ui/src/stores/character-orchestrator.ts index 88e27f28d..373caffed 100644 --- a/packages/stage-ui/src/stores/character-orchestrator.ts +++ b/packages/stage-ui/src/stores/character-orchestrator.ts @@ -20,7 +20,7 @@ import { useProvidersStore } from './providers' interface SparkNotifyCommandDraft { destinations: string[] - interrupt?: 'force' | 'soft' | false + interrupt?: 'force' | 'soft' | boolean priority?: 'critical' | 'high' | 'normal' | 'low' intent?: 'plan' | 'proposal' | 'action' | 'pause' | 'resume' | 'reroute' | 'context' ack?: string @@ -33,36 +33,45 @@ interface SparkNotifyResponse { commands?: SparkNotifyCommandDraft[] } -const SPARK_NOTIFY_INSTRUCTIONS = [ - 'You are processing a spark:notify event for the character.', - 'Respond with a short, in-character reaction as plain text.', - 'If you need to instruct sub-agents, call the spark_command tool one or more times.', -].join('\n') +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.', + `Another module "${moduleName}" triggered spark:notify event for you to checkout.`, + 'You may call the built-in tool "builtIn_sparkCommand" to issue spark:command to sub-agents as needed.', + 'For any of the output that is not a tool call, it will be streamed to user\'s interface and maybe processed with text to speech system ', + 'to be played out loud as your actual reaction to the spark:notify event.', + ].join('\n') +} export const sparkCommandSchema = z.object({ commands: z.array(z.object({ - destinations: z.array(z.string()).min(1), - interrupt: z.union([z.literal('force'), z.literal('soft'), z.literal(false)]).optional(), - priority: z.enum(['critical', 'high', 'normal', 'low']).optional(), - intent: z.enum(['plan', 'proposal', 'action', 'pause', 'resume', 'reroute', 'context']).optional(), - ack: z.string().optional(), + destinations: z.array(z.string()).min(1).describe('List of sub-agent IDs to send the command to'), + interrupt: z.enum(['force', 'soft', 'false']).nullable().describe('Interrupt type: force, soft, or false (no interrupt). A option to control whether this command is urgent enough to preempt ongoing tasks and require immediate attention.'), + priority: z.enum(['critical', 'high', 'normal', 'low']).nullable().describe('Semantic priority of the command, this affects how sub-agents prioritize it (queues, interruption queues, mq, etc.).'), + intent: z.enum(['plan', 'proposal', 'action', 'pause', 'resume', 'reroute', 'context']).nullable().describe('Intent of the command, indicating the nature of the instruction. If you attend to call other tools, use "plan" to reply with quick response to corresponding module / sub-agent.'), + ack: z.string().describe('Acknowledgment content used to be passed to sub-agents upon command receipt.'), guidance: z.object({ type: z.enum(['proposal', 'instruction', 'memory-recall']), - persona: z.record(z.string(), z.enum(['very-high', 'high', 'medium', 'low', 'very-low'])).optional(), + persona: z.array(z.object({ + strength: z.enum(['very-high', 'high', 'medium', 'low', 'very-low']), + traits: z.string().describe('Trait name to adjust behavior. For example, "bravery", "cautiousness", "friendliness".'), + })).nullable().describe('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.'), options: z.array(z.object({ - label: z.string(), - steps: z.array(z.string()), - rationale: z.string().optional(), - possibleOutcome: z.array(z.string()).optional(), - risk: z.enum(['high', 'medium', 'low', 'none']).optional(), - fallback: z.array(z.string()).optional(), - triggers: z.array(z.string()).optional(), + label: z.string().describe('Short and brief label for this option, used for identification, should be within a sentence.'), + steps: z.array(z.string()).describe('Step-by-step instructions for the sub-agent to follow, useful when providing detailed guidance.'), + rationale: z.string().nullable().describe('How this option is derived or proposed, why it makes sense.'), + possibleOutcome: z.array(z.string()).nullable().describe('Simulate possible outcomes of following this option.'), + risk: z.enum(['high', 'medium', 'low', 'none']).nullable(), + fallback: z.array(z.string()).nullable().describe('Fallback steps if the main steps cannot be completed.'), + // TODO: consider to remove or enrich how triggers should work later + triggers: z.array(z.string()).nullable().describe('Conditions or events that would trigger this option.'), })), - }).optional(), - contexts: z.array(z.any()).optional(), - })), + }).nullable().describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'), + })).describe('List of commands to issue to sub-agents, you may produce multiple commands in response to multiple sub-agents by specifying their IDs in destination field. Empty array can be used for zero commands.'), }) +export type SparkCommandSchema = z.infer + export const useCharacterOrchestratorStore = defineStore('character-orchestrator', () => { const { stream } = useLLM() const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore()) @@ -83,14 +92,55 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator const chatProvider = await providersStore.getProviderInstance(activeProvider.value) 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.`, + parameters: z.object({}), + execute: async (_payload) => { + noResponse = true + return 'AIRI System: Acknowledged, no response or action will be processed.' + }, + }) + const sparkCommandTool = await tool({ - name: 'spark_command', - description: `Issue a spark:command to sub-agents.`, + 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.`, parameters: sparkCommandSchema, execute: async (payload) => { try { const validated = await validate(sparkCommandSchema, payload) - commandDrafts.push(...validated.commands) + commandDrafts.push(...validated.commands.map((cmd) => { + const parsedCmd = { + destinations: cmd.destinations, + guidance: cmd.guidance + ? { + type: cmd.guidance.type, + persona: cmd.guidance?.persona?.reduce((acc, curr) => { + acc[curr.traits] = curr.strength + return acc + }, {} as Record) || undefined, + options: cmd.guidance.options.map(opt => ({ + ...opt, + rationale: opt.rationale ?? undefined, + possibleOutcome: opt.possibleOutcome?.length ? opt.possibleOutcome : undefined, + risk: opt.risk ?? undefined, + fallback: opt.fallback?.length ? opt.fallback : undefined, + triggers: opt.triggers?.length ? opt.triggers : undefined, + })), + } + : undefined, + // TODO: contexts can be added later + contexts: [], + priority: cmd.priority || 'normal', + intent: cmd.intent || 'action', + ack: cmd.ack || undefined, + interrupt: cmd.interrupt === 'false' || cmd.interrupt == null ? false : cmd.interrupt, + } satisfies Omit + + return parsedCmd + })) } catch (error) { return `AIRI System: Error - invalid spark_command parameters: ${errorMessageFrom(error)}` @@ -104,7 +154,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator role: 'system', content: [ systemPrompt.value, - SPARK_NOTIFY_INSTRUCTIONS, + getSparkNotifyHandlingAgentInstruction(event.source), ].filter(Boolean).join('\n\n'), } @@ -119,14 +169,25 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator let fullText = '' await stream(activeModel.value, chatProvider, [systemMessage, userMessage], { - tools: [sparkCommandTool], + tools: [ + sparkNoResponseTool, + sparkCommandTool, + ], onStreamEvent: async (streamEvent: StreamEvent) => { if (streamEvent.type === 'text-delta') { + if (noResponse) + return + characterStore.onSparkNotifyReactionStreamEvent(event.data.id, streamEvent.text) fullText += streamEvent.text } if (streamEvent.type === 'finish') { + if (noResponse) { + characterStore.onSparkNotifyReactionStreamEnd(event.data.id, '') + return + } + characterStore.onSparkNotifyReactionStreamEnd(event.data.id, fullText) } if (streamEvent.type === 'error') { @@ -165,7 +226,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator eventId: nanoid(), parentEventId: event.data.id, commandId: nanoid(), - interrupt: command.interrupt ?? false, + interrupt: (command.interrupt === true ? 'force' : command.interrupt) ?? false, priority: command.priority ?? 'normal', intent: command.intent ?? 'action', ack: command.ack,