import crypto from 'node:crypto'; import type { CreatorIntentReadiness, CustomWorldAgentMessage, CustomWorldAgentSessionSnapshot, } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import { buildPendingClarifications, evaluateCreatorIntentReadiness, resolveCreatorIntentStage, } from './customWorldAgentClarificationService.js'; import { buildAnchorPackFromIntent, buildDraftSummaryFromIntent, buildDraftTitleFromIntent, type CustomWorldCreatorIntentRecord, } from './customWorldAgentIntentExtractionService.js'; import type { CustomWorldAgentSessionRecord, CustomWorldAgentSessionStore, } from './customWorldAgentSessionStore.js'; import type { CustomWorldAgentSuggestedActionService } from './customWorldAgentSuggestedActionService.js'; import { CustomWorldAgentSnapshotBuilder } from './customWorldAgentSnapshotBuilder.js'; import { buildCreatorIntentFromEightAnchorContent, buildEightAnchorContentFromCreatorIntent, } from './eightAnchorCompatibilityService.js'; import { EightAnchorSingleTurnService } from './eightAnchorSingleTurnService.js'; function buildDerivedState( intent: CustomWorldCreatorIntentRecord, hasUserInput: boolean, suggestedActionService: CustomWorldAgentSuggestedActionService, ) { const readiness = evaluateCreatorIntentReadiness(intent); const pendingClarifications = buildPendingClarifications(intent, readiness); const stage = resolveCreatorIntentStage({ hasUserInput, readiness, }); return { readiness, pendingClarifications, stage, anchorPack: buildAnchorPackFromIntent(intent, { completedKeys: readiness.completedKeys, missingKeys: readiness.missingKeys, }), draftProfile: { title: buildDraftTitleFromIntent(intent), summary: buildDraftSummaryFromIntent(intent), }, suggestedActions: suggestedActionService.buildSuggestedActions({ stage, isReady: readiness.isReady, }), }; } export class CustomWorldAgentMessageTurnService { constructor( private readonly sessionStore: CustomWorldAgentSessionStore, private readonly eightAnchorSingleTurnService: EightAnchorSingleTurnService, private readonly suggestedActionService: CustomWorldAgentSuggestedActionService, private readonly snapshotBuilder: CustomWorldAgentSnapshotBuilder, ) {} async applyMessageTurn(params: { userId: string; sessionId: string; latestUserText: string; quickFillRequested: boolean; relatedOperationId?: string | null; onReplyUpdate?: (text: string) => void; }) { const latestSession = (await this.sessionStore.get( params.userId, params.sessionId, )) as CustomWorldAgentSessionRecord | null; if (!latestSession) { throw new Error('custom world agent session not found'); } const shouldPreserveDraftStage = (latestSession.stage === 'object_refining' || latestSession.stage === 'visual_refining') && latestSession.draftCards.length > 0; const assistantTurn = await this.eightAnchorSingleTurnService.streamTurn( { currentTurn: latestSession.currentTurn + 1, progressPercent: latestSession.progressPercent, quickFillRequested: params.quickFillRequested, currentAnchorContent: latestSession.anchorContent, chatHistory: latestSession.messages .filter( (message): message is CustomWorldAgentMessage => (message.role === 'user' || message.role === 'assistant') && Boolean(message.text.trim()), ) .map((message) => ({ role: message.role, content: message.text, })), }, { onReplyUpdate: params.onReplyUpdate, }, ); const nextCreatorIntent = buildCreatorIntentFromEightAnchorContent( assistantTurn.nextAnchorContent, ); const progressPercent = Math.max( 0, Math.min(100, Math.round(assistantTurn.progressPercent)), ); const creatorIntentReadiness: CreatorIntentReadiness = progressPercent >= 100 ? { isReady: true, completedKeys: [ 'world_hook', 'player_premise', 'theme_and_tone', 'core_conflict', 'relationship_seed', 'iconic_element', ], missingKeys: [], } : evaluateCreatorIntentReadiness(nextCreatorIntent); const derivedState = buildDerivedState( nextCreatorIntent, true, this.suggestedActionService, ); const shouldStayInDraftStage = shouldPreserveDraftStage && progressPercent >= 100; const assistantMessage = { id: `message-${crypto.randomBytes(8).toString('hex')}`, role: 'assistant', kind: 'chat', text: assistantTurn.replyText, createdAt: new Date().toISOString(), relatedOperationId: params.relatedOperationId ?? null, } satisfies CustomWorldAgentMessage; await this.sessionStore.replaceDerivedState( params.userId, params.sessionId, this.snapshotBuilder.buildMessageTurnState({ latestSession, nextAnchorContent: assistantTurn.nextAnchorContent, progressPercent, replyText: assistantTurn.replyText, nextCreatorIntent, creatorIntentReadiness, derivedDraftProfile: derivedState.draftProfile, derivedPendingClarifications: derivedState.pendingClarifications, derivedStage: derivedState.stage, shouldStayInDraftStage, }), ); await this.sessionStore.appendMessage( params.userId, params.sessionId, assistantMessage, ); return (await this.sessionStore.getSnapshot( params.userId, params.sessionId, )) as CustomWorldAgentSessionSnapshot; } deriveInitialSessionState(params: { seedText: string; creatorIntent: CustomWorldCreatorIntentRecord; }) { const anchorContent = buildEightAnchorContentFromCreatorIntent( params.creatorIntent, ); const derivedState = buildDerivedState( params.creatorIntent, Boolean(params.seedText), this.suggestedActionService, ); return { anchorContent, ...derivedState, }; } }