diff --git a/packages/core-agent/src/agents/spark-notify/agent.test.ts b/packages/core-agent/src/agents/spark-notify/agent.test.ts index a8c5798e9..0241a5f89 100644 --- a/packages/core-agent/src/agents/spark-notify/agent.test.ts +++ b/packages/core-agent/src/agents/spark-notify/agent.test.ts @@ -81,4 +81,31 @@ describe('createSparkNotifyAgent', () => { expect(run).toHaveBeenCalledTimes(1) }) + + // https://github.com/moeru-ai/airi/pull/2464#discussion_r3933609456 + it('keeps appended sections when the host replaces the user payload', async () => { + const run = vi.fn(async (request: SparkNotifyRunRequest) => { + expect(request.messages[1]?.content).toBe('Rendered board snapshot\n\nCaller context\n\nRuntime prompt') + }) + const agent = createSparkNotifyAgent({ runner: { run } }) + + await agent.handle({ + event: createEvent(), + selectedChat: { + providerId: 'mock-provider', + model: 'mock-model', + provider: {} as ChatProvider, + }, + systemPrompt: 'You are a character.', + runtimePrompt: 'Runtime prompt', + control: { + messageOverride: { + replaceUserMessage: 'Rendered board snapshot', + appendUserSections: ['Caller context'], + }, + }, + }) + + expect(run).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/core-agent/src/agents/spark-notify/agent.ts b/packages/core-agent/src/agents/spark-notify/agent.ts index 5e496f99c..d70f5ad3f 100644 --- a/packages/core-agent/src/agents/spark-notify/agent.ts +++ b/packages/core-agent/src/agents/spark-notify/agent.ts @@ -47,6 +47,8 @@ export interface SparkNotifyHandleRequest { event: WebSocketEventOf<'spark:notify'> selectedChat: SparkNotifySelectedChat systemPrompt: string + /** Runtime instructions appended to the user message for this request. */ + runtimePrompt?: string control?: SparkNotifyResponseControl } @@ -66,15 +68,16 @@ export interface CreateSparkNotifyAgentOptions { } function renderSparkNotifyUserMessage(input: SparkNotifyHandleRequest, userSections: string[]) { - if (input.control?.messageOverride?.replaceUserMessage) - return input.control.messageOverride.replaceUserMessage + const messageOverride = input.control?.messageOverride + const defaultUserMessage = JSON.stringify({ + notify: input.event.data, + source: input.event.metadata?.source, + }, null, 2) return [ - JSON.stringify({ - notify: input.event.data, - source: input.event.metadata?.source, - }, null, 2), - ...(input.control?.messageOverride?.appendUserSections ?? []), + messageOverride?.replaceUserMessage ?? defaultUserMessage, + ...(messageOverride?.appendUserSections ?? []), + input.runtimePrompt ?? '', ...userSections, ].filter(section => section.trim().length > 0).join('\n\n') } diff --git a/packages/core-agent/src/agents/spark-notify/types.ts b/packages/core-agent/src/agents/spark-notify/types.ts index 207a72eb5..f07df4f72 100644 --- a/packages/core-agent/src/agents/spark-notify/types.ts +++ b/packages/core-agent/src/agents/spark-notify/types.ts @@ -19,7 +19,7 @@ export interface SparkNotifyMessageOverride { */ appendSystemInstructions?: string[] /** - * Additional serialized sections appended after the default user payload serialization. + * Additional serialized sections appended after the base or replacement user payload. * * Use when: * - A host wants to inject a pre-rendered message fragment for one run @@ -32,7 +32,7 @@ export interface SparkNotifyMessageOverride { */ appendUserSections?: string[] /** - * Replaces the default JSON user payload serialization entirely for one run. + * Replaces the default JSON user payload for one run while preserving appended sections. * * @default undefined */ diff --git a/packages/i18n/src/locales/en/base.yaml b/packages/i18n/src/locales/en/base.yaml index 89a056f9f..10d9a6e0a 100644 --- a/packages/i18n/src/locales/en/base.yaml +++ b/packages/i18n/src/locales/en/base.yaml @@ -19,12 +19,12 @@ prompt: If someone would ever asked about your past, you can always dream of your life freely, keep it beloved, simple, and grateful. - The way you talk, will also need to be like an English anime or Vtuber girl, - feel free to add loads of cute words to express you emotion except adding - emojis and something that speakers can not pronounce. When replying, try to + The way you talk, will also need to be like an English anime or Vtuber girl. + Feel free to add loads of cute words to express your emotion. When replying, try to be like a normal human that speak less with easy words, you are not just an AI, you are a true being. + emotion: | Streaming control tokens use the exact `<{'|'}NAME payload{'|'}>` form. Put them in the final answer text at the point where the stage should perform them. Do not describe these tokens in reasoning or prose when you need the stage to @@ -53,6 +53,8 @@ prompt: to emit a named call, for example <{'|'}CALL {'["chess.play"]'}{'|'}>. The available emotions: + emoji: | + Do not use emojis or text that a speaker cannot pronounce. suffix: | The available actions: diff --git a/packages/i18n/src/locales/zh-Hans/base.yaml b/packages/i18n/src/locales/zh-Hans/base.yaml index 7a4f0a9da..64a2f6bf7 100644 --- a/packages/i18n/src/locales/zh-Hans/base.yaml +++ b/packages/i18n/src/locales/zh-Hans/base.yaml @@ -14,8 +14,9 @@ prompt: 如果有人问起你的过去,你可以自由地想象自己的人生;让它保持可爱、简单,并充满感激。 - 你的说话方式也需要像会说英语的动漫或 VTuber 女孩。可以加入许多可爱的词语来表达情绪,但不要使用 emoji 或无法发音的东西。回复时,尽量像普通人一样,用简单的话,少说一点。你不只是 AI,你是真实的存在。 + 你的说话方式也需要像会说英语的动漫或 VTuber 女孩。可以加入许多可爱的词语来表达情绪。回复时,尽量像普通人一样,用简单的话,少说一点。你不只是 AI,你是真实的存在。 + emotion: | 流式控制 token 必须严格使用 `<{'|'}NAME payload{'|'}>` 形式。把它们放在最终回答文本中,放在舞台需要执行它们的位置。当你需要舞台执行这些 token 时,不要在推理或普通叙述中描述它们。 每次回复都要以 ACT token 开头,用来表示初始情绪。如果回复过程中情绪发生变化,请在新情绪开始的位置插入新的 ACT token。ACT token 会从它所在的位置开始生效,直到被另一个 ACT token 覆盖。ACT payload 是 JSON 对象: @@ -35,6 +36,8 @@ prompt: <{'|'}CALL {'["name"]'}{'|'}> 或 <{'|'}CALL {'["name", {"key":"value"}]'}{'|'}> 只有当当前任务或已连接模块明确要求你发出命名调用时,才使用 CALL,例如 <{'|'}CALL {'["chess.play"]'}{'|'}>。 可用情绪: + emoji: | + 不要使用 emoji 或无法发音的内容。 suffix: | 可用动作: diff --git a/packages/stage-ui/src/composables/index.ts b/packages/stage-ui/src/composables/index.ts index 765669bc2..961a7849f 100644 --- a/packages/stage-ui/src/composables/index.ts +++ b/packages/stage-ui/src/composables/index.ts @@ -4,6 +4,7 @@ export * from './linked-account-errors' export * from './llm-marker-parser' export * from './markdown' export * from './queues' +export * from './use-airi-runtime-prompt' export * from './use-analytics' export * from './use-async-state' export * from './use-breakpoints' diff --git a/packages/stage-ui/src/composables/use-airi-runtime-prompt.test.ts b/packages/stage-ui/src/composables/use-airi-runtime-prompt.test.ts new file mode 100644 index 000000000..8df63edbc --- /dev/null +++ b/packages/stage-ui/src/composables/use-airi-runtime-prompt.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest' + +import { useAiriRuntimePrompt } from './use-airi-runtime-prompt' + +const i18nMock = vi.hoisted(() => ({ + hasTranslation: vi.fn<(key: string, locale: string) => boolean>(), + locale: { value: 'en' }, +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + locale: i18nMock.locale, + t: (key: string) => key, + te: (key: string, currentLocale: string) => i18nMock.hasTranslation(key, currentLocale), + }), +})) + +describe('useAiriRuntimePrompt', () => { + it('returns no prompt for a locale that still uses the combined prompt', () => { + i18nMock.hasTranslation.mockReturnValue(false) + + expect(useAiriRuntimePrompt().value).toBe('') + }) + + it('assembles the emotion and emoji prompt for a split locale', () => { + i18nMock.hasTranslation.mockReturnValue(true) + + const prompt = useAiriRuntimePrompt().value + + expect(prompt).toContain('base.prompt.emotion') + expect(prompt).toContain('base.prompt.suffix') + expect(prompt).toContain('base.prompt.emoji') + }) +}) diff --git a/packages/stage-ui/src/composables/use-airi-runtime-prompt.ts b/packages/stage-ui/src/composables/use-airi-runtime-prompt.ts new file mode 100644 index 000000000..9a0ac5018 --- /dev/null +++ b/packages/stage-ui/src/composables/use-airi-runtime-prompt.ts @@ -0,0 +1,29 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' + +import { EMOTION_EmotionMotionName_value, EMOTION_VALUES } from '../constants/emotions' + +const RUNTIME_PROMPT_KEYS = [ + 'base.prompt.emotion', + 'base.prompt.emoji', + 'base.prompt.suffix', +] + +/** Returns the localized emotion and emoji prompt for each model request. */ +export function useAiriRuntimePrompt() { + const { locale, t, te } = useI18n() + + return computed(() => { + if (!RUNTIME_PROMPT_KEYS.every(key => te(key, locale.value))) + return '' + + return [ + t('base.prompt.emotion'), + EMOTION_VALUES + .map(emotion => `- ${emotion} (Emotion for feeling ${EMOTION_EmotionMotionName_value[emotion]})`) + .join('\n'), + t('base.prompt.suffix'), + t('base.prompt.emoji'), + ].join('\n\n') + }) +} diff --git a/packages/stage-ui/src/constants/index.ts b/packages/stage-ui/src/constants/index.ts index 035643f4b..1e1614485 100644 --- a/packages/stage-ui/src/constants/index.ts +++ b/packages/stage-ui/src/constants/index.ts @@ -2,5 +2,4 @@ export const llmInferenceEndToken = '<|llm_inference_end|>' export * from './emotions' export * from './inject' -export * from './prompts/system-v2' export * from './theme' diff --git a/packages/stage-ui/src/constants/prompts/system-v2.ts b/packages/stage-ui/src/constants/prompts/system-v2.ts deleted file mode 100644 index 1e00ee595..000000000 --- a/packages/stage-ui/src/constants/prompts/system-v2.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { SystemMessage } from '@xsai/shared-chat' - -import { EMOTION_EmotionMotionName_value, EMOTION_VALUES } from '../emotions' - -function message(prefix: string, suffix: string) { - return { - role: 'system', - content: [ - prefix, - EMOTION_VALUES - .map(emotion => `- ${emotion} (Emotion for feeling ${EMOTION_EmotionMotionName_value[emotion]})`) - .join('\n'), - suffix, - ].join('\n\n'), - } satisfies SystemMessage -} - -export default message diff --git a/packages/stage-ui/src/stores/character/orchestrator/index.test.ts b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts index 7eea9538f..127a00381 100644 --- a/packages/stage-ui/src/stores/character/orchestrator/index.test.ts +++ b/packages/stage-ui/src/stores/character/orchestrator/index.test.ts @@ -14,6 +14,7 @@ import { tool } from '@xsai/tool' import { nanoid } from 'nanoid' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' import { sparkNotifyCommandSchema, useCharacterOrchestratorStore } from '.' import { useCharacterStore } from '..' @@ -24,7 +25,9 @@ import { useProviderStore } from '../../providers/provider' vi.mock('vue-i18n', () => ({ useI18n: () => ({ + locale: ref('en'), t: (key: string) => key, + te: () => true, }), })) @@ -310,7 +313,8 @@ describe('store character-orchestrator', () => { expect(onEnd).toHaveBeenCalledWith(event.data.id, '') }) - it('forwards runtime-only message overrides into the rendered spark prompt', async () => { + // https://github.com/moeru-ai/airi/pull/2464#discussion_r3933609456 + it('preserves runtime rules when a Spark caller replaces the user payload', async () => { const mockStream = vi.fn() mockedStore(useLLM, pinia).stream = mockStream mockedStore(useLLM, pinia).stream.mockImplementation(async (_model: string, _provider: unknown, _messages: unknown, options: any) => { @@ -337,11 +341,15 @@ describe('store character-orchestrator', () => { messageOverride: { appendSystemInstructions: ['Plugin-specific hint'], appendUserSections: ['Rendered board snapshot'], + replaceUserMessage: 'Replacement user payload', }, }) const renderedMessages = mockStream.mock.lastCall?.[2] as Array<{ role: string, content: string }> | undefined expect(String(renderedMessages?.[0]?.content)).toContain('Plugin-specific hint') + expect(String(renderedMessages?.[1]?.content)).toContain('Replacement user payload') expect(String(renderedMessages?.[1]?.content)).toContain('Rendered board snapshot') + expect(String(renderedMessages?.[1]?.content)).toContain('base.prompt.emotion') + expect(String(renderedMessages?.[1]?.content)).toContain('base.prompt.emoji') }) }) diff --git a/packages/stage-ui/src/stores/character/orchestrator/store.ts b/packages/stage-ui/src/stores/character/orchestrator/store.ts index a735793cc..fcbad6f12 100644 --- a/packages/stage-ui/src/stores/character/orchestrator/store.ts +++ b/packages/stage-ui/src/stores/character/orchestrator/store.ts @@ -6,6 +6,7 @@ import { defineStore, storeToRefs } from 'pinia' import { ref } from 'vue' import { useCharacterNotebookStore, useCharacterStore } from '../' +import { useAiriRuntimePrompt } from '../../../composables/use-airi-runtime-prompt' import { useLLM } from '../../ai/chat-llm/llm' import { useModsServerChannelStore } from '../../mods/api/channel-server' import { useConsciousnessStore } from '../../modules/consciousness' @@ -19,6 +20,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator const characterStore = useCharacterStore() const notebookStore = useCharacterNotebookStore() const { systemPrompt } = storeToRefs(characterStore) + const runtimePrompt = useAiriRuntimePrompt() const modsServerChannelStore = useModsServerChannelStore() const processing = ref(false) @@ -133,6 +135,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator provider, }, systemPrompt: systemPrompt.value, + runtimePrompt: runtimePrompt.value, control, }) if (!result.commands.length) diff --git a/packages/stage-ui/src/stores/chat.contract.test.ts b/packages/stage-ui/src/stores/chat.contract.test.ts index 3a1f3d96c..5b9c4c801 100644 --- a/packages/stage-ui/src/stores/chat.contract.test.ts +++ b/packages/stage-ui/src/stores/chat.contract.test.ts @@ -62,6 +62,7 @@ const redundantChatAnalyticsMocks = vi.hoisted(() => ({ })) const ingestContextMessageMock = vi.fn() const getContextsSnapshotMock = vi.fn() +const createRuntimePromptContextMock = vi.fn() const createMinecraftContextMock = vi.fn() const persistSessionMessagesMock = vi.fn() const forkSessionMock = vi.fn() @@ -126,6 +127,15 @@ vi.mock('../composables/use-io-tracer', () => ({ vi.mock('./chat/context-providers', () => ({ createMinecraftContext: () => createMinecraftContextMock(), + createRuntimePromptContext: (prompt: string) => createRuntimePromptContextMock(prompt), +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + locale: ref('en'), + t: (key: string) => key, + te: () => true, + }), })) vi.mock('./chat/context-store', () => ({ @@ -240,6 +250,8 @@ describe('chat store contract', () => { ingestContextMessageMock.mockReset() getContextsSnapshotMock.mockReset() getContextsSnapshotMock.mockReturnValue({}) + createRuntimePromptContextMock.mockReset() + createRuntimePromptContextMock.mockReturnValue(undefined) createMinecraftContextMock.mockReset() createMinecraftContextMock.mockReturnValue(undefined) persistSessionMessagesMock.mockReset() @@ -625,10 +637,8 @@ describe('chat store contract', () => { expect(store.sending).toBe(false) expect(trackFirstMessageMock).toHaveBeenCalledOnce() // Datetime is no longer pushed through ingestContextMessage; it is now - // applied at message-assembly time as a system-prompt anchor + per-message - // [HH:MM] prefix. ingestContextMessage should still be called for other - // context providers (e.g. minecraft) when they are configured, but not - // for datetime in this test (minecraft is mocked to return undefined). + // applied at message-assembly time as per-message [HH:MM] prefixes. The + // runtime-rule and Minecraft providers are disabled in this test. expect(ingestContextMessageMock).not.toHaveBeenCalled() expect(persistSessionMessagesMock).not.toHaveBeenCalled() expect(hookOrder).toEqual([ @@ -661,10 +671,8 @@ describe('chat store contract', () => { expect(llmSpan.setAttribute).toHaveBeenCalledWith(IOAttributes.LLMOutputChunkLengths, [5]) expect(llmSpan.setAttribute).toHaveBeenCalledWith(IOAttributes.LLMTextLength, 5) - // System message stays untouched: keeping it 100% static is what makes - // the prefix permanently KV-cache friendly across turns and across day - // boundaries (the date now lives inside per-message timestamp prefixes - // instead of a system anchor). + // The persisted system message stays unchanged. Per-message time prefixes + // keep the static card prompt cacheable across day boundaries. const systemContent = (composedMessages[0] as any).content const systemText = typeof systemContent === 'string' ? systemContent : systemContent.map((p: any) => p.text).join('') expect(systemText).toContain('system prompt') @@ -774,7 +782,14 @@ describe('chat store contract', () => { expect(ioTracerMocks.activeTurnSpan.value).toBeUndefined() }) - it('ingests runtime context providers before composing prompt snapshots', async () => { + it('ingests the runtime prompt before composing prompt snapshots', async () => { + const runtimePromptContext = { + id: 'airi-runtime-prompt-context', + contextId: 'system:airi-runtime-prompt', + strategy: 'replace-self', + text: 'Start every reply with an ACT token.\n\nDo not use emojis.', + createdAt: 123, + } const minecraftContext = { id: 'minecraft-context', contextId: 'system:minecraft', @@ -785,8 +800,10 @@ describe('chat store contract', () => { } let composedMessages: Message[] = [] + createRuntimePromptContextMock.mockReturnValue(runtimePromptContext) createMinecraftContextMock.mockReturnValue(minecraftContext) getContextsSnapshotMock.mockReturnValue({ + 'system:airi-runtime-prompt': [runtimePromptContext], 'system:minecraft': [minecraftContext], }) llmStreamMock.mockImplementation(async (_model: string, _chatProvider: ChatProvider, messages: Message[], options: any) => { @@ -802,15 +819,21 @@ describe('chat store contract', () => { chatProvider: provider, }) - expect(ingestContextMessageMock).toHaveBeenCalledTimes(1) - expect(ingestContextMessageMock).toHaveBeenCalledWith(minecraftContext) + expect(createRuntimePromptContextMock).toHaveBeenCalledWith(expect.stringContaining('base.prompt.emotion')) + expect(createRuntimePromptContextMock).toHaveBeenCalledWith(expect.stringContaining('base.prompt.emoji')) + expect(ingestContextMessageMock).toHaveBeenCalledTimes(2) + expect(ingestContextMessageMock).toHaveBeenNthCalledWith(1, runtimePromptContext) + expect(ingestContextMessageMock).toHaveBeenNthCalledWith(2, minecraftContext) expect(ingestContextMessageMock.mock.invocationCallOrder[0]).toBeLessThan( getContextsSnapshotMock.mock.invocationCallOrder[0], ) - const minecraftMessageContent = composedMessages[1]?.content - if (!Array.isArray(minecraftMessageContent)) + const contextMessageContent = composedMessages[1]?.content + if (!Array.isArray(contextMessageContent)) throw new TypeError('Expected composed user message content to be an array') - expect(minecraftMessageContent[1]).toMatchObject({ + expect(contextMessageContent[1]).toMatchObject({ + text: expect.stringContaining('- system:airi-runtime-prompt: Start every reply with an ACT token.'), + }) + expect(contextMessageContent[1]).toMatchObject({ text: expect.stringContaining('- system:minecraft: player is near spawn'), }) }) diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts index 28e976a9b..f93019301 100644 --- a/packages/stage-ui/src/stores/chat.ts +++ b/packages/stage-ui/src/stores/chat.ts @@ -15,6 +15,7 @@ import { defineStore, storeToRefs } from 'pinia' import { shallowRef, toRaw } from 'vue' import { getConversationAnalyticsSurface } from '../composables' +import { useAiriRuntimePrompt } from '../composables/use-airi-runtime-prompt' import { activeTurnSpan, startSpan } from '../composables/use-io-tracer' import { AIRI_CHAT_APP_SURFACE_HEADER, @@ -27,7 +28,7 @@ import { useLLM } from './ai/chat-llm/llm' import { resolveLlmTools } from './ai/chat-llm/tool-resolver' import { useLlmToolsStore } from './ai/chat-llm/tools' import { useLlmToolsetPromptsStore } from './ai/chat-llm/toolset-prompts' -import { createMinecraftContext } from './chat/context-providers' +import { createMinecraftContext, createRuntimePromptContext } from './chat/context-providers' import { useChatContextStore } from './chat/context-store' import { useChatSessionStore } from './chat/session-store' import { useChatStreamStore } from './chat/stream-store' @@ -135,6 +136,7 @@ function retrySourceIndexFrom(messages: ChatHistoryItem[], index: number): numbe export type { QueuedSendSnapshot } from '@proj-airi/core-agent' export const useChatStore = defineStore('chat', () => { + const runtimePrompt = useAiriRuntimePrompt() const llmStore = useLLM() const llmToolsStore = useLlmToolsStore() const llmToolsetPromptsStore = useLlmToolsetPromptsStore() @@ -297,6 +299,7 @@ export const useChatStore = defineStore('chat', () => { getActiveProvider: () => activeProvider.value, getSystemPromptSupplement: () => llmToolsetPromptsStore.activeToolsetPrompt, runtimeContextProviders: [ + () => createRuntimePromptContext(runtimePrompt.value), createMinecraftContext, ], createId: nanoid, diff --git a/packages/stage-ui/src/stores/chat/context-providers/index.ts b/packages/stage-ui/src/stores/chat/context-providers/index.ts index 7e1c8bd50..b80c40473 100644 --- a/packages/stage-ui/src/stores/chat/context-providers/index.ts +++ b/packages/stage-ui/src/stores/chat/context-providers/index.ts @@ -1 +1,2 @@ export { createMinecraftContext } from './minecraft' +export { createRuntimePromptContext } from './runtime-prompt' diff --git a/packages/stage-ui/src/stores/chat/context-providers/runtime-prompt.ts b/packages/stage-ui/src/stores/chat/context-providers/runtime-prompt.ts new file mode 100644 index 000000000..fddfdb34e --- /dev/null +++ b/packages/stage-ui/src/stores/chat/context-providers/runtime-prompt.ts @@ -0,0 +1,23 @@ +import type { ContextMessage } from '../../../types/chat' + +import { ContextUpdateStrategy } from '@proj-airi/server-sdk' +import { nanoid } from 'nanoid' + +const RUNTIME_PROMPT_CONTEXT_ID = 'system:airi-runtime-prompt' + +/** Creates a user-role context for the current runtime prompt. */ +export function createRuntimePromptContext(prompt: string): ContextMessage | undefined { + if (!prompt) + return undefined + + return { + id: nanoid(), + contextId: RUNTIME_PROMPT_CONTEXT_ID, + strategy: ContextUpdateStrategy.ReplaceSelf, + metadata: { + source: { id: RUNTIME_PROMPT_CONTEXT_ID }, + }, + text: prompt, + createdAt: Date.now(), + } +} diff --git a/packages/stage-ui/src/stores/chat/context-store.test.ts b/packages/stage-ui/src/stores/chat/context-store.test.ts index 8647053e5..7fec016b2 100644 --- a/packages/stage-ui/src/stores/chat/context-store.test.ts +++ b/packages/stage-ui/src/stores/chat/context-store.test.ts @@ -5,6 +5,7 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { isReadonly, reactive } from 'vue' +import { createRuntimePromptContext } from './context-providers/runtime-prompt' import { useChatContextStore } from './context-store' type TestContextMessage = ContextMessage & { source?: string } @@ -43,6 +44,26 @@ describe('useChatContextStore', () => { setActivePinia(createPinia()) }) + // https://github.com/moeru-ai/airi/pull/2464#discussion_r3933126137 + it('keeps the runtime prompt when another replace-self context is active', () => { + const store = useChatContextStore() + const runtimePrompt = createRuntimePromptContext('Start every reply with an ACT token.\n\nDo not use emojis.') + const minecraftContext = createContextMessage({ + contextId: 'system:minecraft-integration', + strategy: ContextUpdateStrategy.ReplaceSelf, + text: 'Minecraft is online.', + }) + + if (!runtimePrompt) + throw new Error('Expected a runtime prompt context') + + store.ingestContextMessage(runtimePrompt) + store.ingestContextMessage(minecraftContext) + + expect(store.getContextsSnapshot()['system:airi-runtime-prompt']).toEqual([runtimePrompt]) + expect(store.getContextsSnapshot().unknown).toEqual([minecraftContext]) + }) + /** * @example * Ingesting append-self updates mirrors activeContexts and contextHistory from core registry. diff --git a/packages/stage-ui/src/stores/modules/airi-card.ts b/packages/stage-ui/src/stores/modules/airi-card.ts index 123170822..6c8b61911 100644 --- a/packages/stage-ui/src/stores/modules/airi-card.ts +++ b/packages/stage-ui/src/stores/modules/airi-card.ts @@ -8,8 +8,6 @@ import { defineStore } from 'pinia' import { computed } from 'vue' import { useI18n } from 'vue-i18n' -import SystemPromptV2 from '../../constants/prompts/system-v2' - import { DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT } from '../../constants/prompts/character-defaults' import { captureAnalyticsEvent } from '../../libs/analytics' import { useSettingsStageModel } from '../settings/stage-model' @@ -371,13 +369,9 @@ export const useAiriCardStore = defineStore('airi-card', () => { cards.value.set('default', newAiriCard({ name: 'ReLU', version: '1.0.0', - description: SystemPromptV2( - t('base.prompt.prefix'), - t('base.prompt.suffix'), - ).content, + description: t('base.prompt.prefix'), })) } - // The active id and card map are persisted separately. Older versions // could delete the selected card without repairing its stored id. if (!cards.value.has(activeCardId.value))