import type { QuestGenerationRequest } from '../../../packages/shared/src/contracts/story.js'; import { QUEST_INTIMACY_LEVELS, QUEST_NARRATIVE_TYPES, QUEST_OBJECTIVE_KINDS, QUEST_REWARD_THEMES, QUEST_URGENCY_LEVELS, } from '../../../packages/shared/src/contracts/story.js'; import { parseJsonResponseText } from '../../../packages/shared/src/llm/parsers.js'; import { buildFallbackQuestIntent, compileQuestIntentToQuest, evaluateQuestOpportunity, buildQuestGenerationContextFromState, buildQuestIntentPrompt, QUEST_INTENT_SYSTEM_PROMPT, } from '../bridges/legacyQuestRuntimeBridge.js'; import type { UpstreamLlmClient } from './llmClient.js'; type QuestPreviewRequest = Parameters[0]; type QuestIntent = ReturnType; type QuestGenerationInput = Parameters[0]; type QuestGenerationState = QuestGenerationInput['state']; type QuestGenerationEncounter = QuestGenerationInput['encounter']; type QuestLogEntry = ReturnType; function coerceString(value: unknown, fallback: string) { return typeof value === 'string' && value.trim() ? value.trim() : fallback; } function coerceStringArray(value: unknown, fallback: string[]) { if (!Array.isArray(value)) { return fallback; } const items = value .map((item) => (typeof item === 'string' ? item.trim() : '')) .filter(Boolean); return items.length > 0 ? items : fallback; } function sanitizeQuestIntent(rawIntent: unknown, fallback: QuestIntent): QuestIntent { if (!rawIntent || typeof rawIntent !== 'object') { return fallback; } const intent = rawIntent as Record; return { title: coerceString(intent.title, fallback.title), description: coerceString(intent.description, fallback.description), summary: coerceString(intent.summary, fallback.summary), narrativeType: typeof intent.narrativeType === 'string' && QUEST_NARRATIVE_TYPES.includes(intent.narrativeType) ? (intent.narrativeType as QuestIntent['narrativeType']) : fallback.narrativeType, dramaticNeed: coerceString(intent.dramaticNeed, fallback.dramaticNeed), issuerGoal: coerceString(intent.issuerGoal, fallback.issuerGoal), playerHook: coerceString(intent.playerHook, fallback.playerHook), worldReason: coerceString(intent.worldReason, fallback.worldReason), recommendedObjectiveKinds: coerceStringArray( intent.recommendedObjectiveKinds, fallback.recommendedObjectiveKinds, ).filter((kind) => QUEST_OBJECTIVE_KINDS.includes(kind)) as QuestIntent['recommendedObjectiveKinds'], urgency: typeof intent.urgency === 'string' && QUEST_URGENCY_LEVELS.includes(intent.urgency) ? (intent.urgency as QuestIntent['urgency']) : fallback.urgency, intimacy: typeof intent.intimacy === 'string' && QUEST_INTIMACY_LEVELS.includes(intent.intimacy) ? (intent.intimacy as QuestIntent['intimacy']) : fallback.intimacy, rewardTheme: typeof intent.rewardTheme === 'string' && QUEST_REWARD_THEMES.includes(intent.rewardTheme) ? (intent.rewardTheme as QuestIntent['rewardTheme']) : fallback.rewardTheme, followupHooks: coerceStringArray(intent.followupHooks, fallback.followupHooks), }; } export async function generateQuestForNpcEncounter( llmClient: UpstreamLlmClient, params: QuestGenerationRequest, ): Promise { const { state, encounter } = params; const issuerNpcId = encounter.id ?? encounter.npcName; const request: QuestPreviewRequest = { issuerNpcId, issuerNpcName: encounter.npcName, roleText: encounter.context, scene: state.currentScenePreset, worldType: state.worldType, currentQuests: state.quests.map((quest) => ({ id: quest.id, issuerNpcId: quest.issuerNpcId, status: quest.status, })), context: buildQuestGenerationContextFromState({ state, encounter }), origin: 'ai_compiled', }; const opportunity = evaluateQuestOpportunity(request); if (!opportunity.shouldOffer) { return null; } const fallbackIntent = buildFallbackQuestIntent(request); try { const content = await llmClient.requestMessageContent({ systemPrompt: QUEST_INTENT_SYSTEM_PROMPT, userPrompt: buildQuestIntentPrompt({ context: request.context!, scene: request.scene, opportunity, }), }); const parsed = parseJsonResponseText(content) as { intent?: unknown }; const intent = sanitizeQuestIntent(parsed.intent, fallbackIntent); return compileQuestIntentToQuest( { ...request, origin: 'ai_compiled', }, intent, ); } catch { return compileQuestIntentToQuest( { ...request, origin: 'fallback_builder', }, fallbackIntent, ); } }