Important Fixes, New mechanics, and many more
Some checks failed
CI / validate (push) Failing after 9m21s
Some checks failed
CI / validate (push) Failing after 9m21s
This commit is contained in:
110
apps/worker/src/orchestration.ts
Normal file
110
apps/worker/src/orchestration.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
RoundContextSchema,
|
||||
RoundPlanSchema,
|
||||
type Character,
|
||||
type PlayerIntent,
|
||||
type RoundContext,
|
||||
type RoundPlan,
|
||||
type RoundResolution,
|
||||
} from '@dng/shared'
|
||||
|
||||
const postgresUuidPattern = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i
|
||||
|
||||
export interface RoundContextInput {
|
||||
scene: string
|
||||
storySummary?: string | null
|
||||
recentRounds: Array<{ number: number; narration: string; nextPrompt?: string | null }>
|
||||
intents: PlayerIntent[]
|
||||
characters: Character[]
|
||||
memories: Array<{ summary: string; importance?: number; tags?: string[]; entityIds?: string[] }>
|
||||
}
|
||||
|
||||
export function buildRoundContext(input: RoundContextInput): RoundContext {
|
||||
const characterIds = new Set(input.characters.map(character => character.id))
|
||||
const actingCharacterIds = new Set(input.intents.filter(intent => intent.ready).map(intent => intent.characterId))
|
||||
const humanIntents = input.intents
|
||||
.filter(intent => intent.ready && characterIds.has(intent.characterId))
|
||||
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))
|
||||
const aiCharacters = input.characters.filter(character =>
|
||||
character.controller === 'ai'
|
||||
|| (character.controller === 'delegated' && !actingCharacterIds.has(character.id)),
|
||||
)
|
||||
const relevanceText = [
|
||||
input.scene,
|
||||
...humanIntents.map(intent => intent.action),
|
||||
...input.characters.flatMap(character => [character.id, character.name]),
|
||||
].join(' ').toLowerCase()
|
||||
const rankedMemories = input.memories
|
||||
.map(memory => {
|
||||
const terms = [...(memory.tags ?? []), ...(memory.entityIds ?? [])]
|
||||
const relevance = terms.reduce((score, term) => score + (term && relevanceText.includes(term.toLowerCase()) ? 10 : 0), 0)
|
||||
return { memory, score: relevance + (memory.importance ?? 0) }
|
||||
})
|
||||
.sort((left, right) => right.score - left.score)
|
||||
.slice(0, 8)
|
||||
.map(entry => entry.memory)
|
||||
|
||||
return RoundContextSchema.parse({
|
||||
scene: input.scene,
|
||||
storySummary: input.storySummary ?? null,
|
||||
recentRounds: [...input.recentRounds].sort((a, b) => a.number - b.number).slice(-2),
|
||||
humanIntents,
|
||||
aiCharacters,
|
||||
activeCharacters: input.characters,
|
||||
memories: rankedMemories,
|
||||
})
|
||||
}
|
||||
|
||||
export function validateAndOrderRoundPlan(planInput: unknown, context: RoundContext): RoundPlan {
|
||||
const plan = RoundPlanSchema.parse(planInput)
|
||||
const characterIds = new Set(context.activeCharacters.map(character => character.id))
|
||||
const aiOrder = new Map(context.aiCharacters.map((character, index) => [character.id, index]))
|
||||
const seenAiActors = new Set<string>()
|
||||
|
||||
for (const check of plan.checks) {
|
||||
if (!characterIds.has(check.actorId)) throw new Error(`Unknown check actor ${check.actorId}`)
|
||||
if (check.targetId && !characterIds.has(check.targetId)) throw new Error(`Unknown check target ${check.targetId}`)
|
||||
}
|
||||
for (const event of plan.proposedEvents) {
|
||||
if (event.actorId && !characterIds.has(event.actorId)) throw new Error(`Unknown event actor ${event.actorId}`)
|
||||
if (event.targetId && !characterIds.has(event.targetId)) throw new Error(`Unknown event target ${event.targetId}`)
|
||||
if (['damage', 'healing', 'inventory', 'status'].includes(event.type) && !event.targetId) {
|
||||
throw new Error(`${event.type} event requires a target`)
|
||||
}
|
||||
}
|
||||
|
||||
const aiActions = plan.aiActions
|
||||
.filter(action => aiOrder.has(action.characterId))
|
||||
.filter(action => {
|
||||
if (seenAiActors.has(action.characterId)) return false
|
||||
seenAiActors.add(action.characterId)
|
||||
return true
|
||||
})
|
||||
.sort((left, right) => aiOrder.get(left.characterId)! - aiOrder.get(right.characterId)!)
|
||||
|
||||
return { ...plan, aiActions }
|
||||
}
|
||||
|
||||
export function buildActionSequence(context: RoundContext, plan: RoundPlan) {
|
||||
return [
|
||||
...context.humanIntents.map(intent => ({ phase: 'human' as const, characterId: intent.characterId, action: intent.action })),
|
||||
...plan.aiActions.map(action => ({ phase: 'ai' as const, characterId: action.characterId, action: action.action })),
|
||||
]
|
||||
}
|
||||
|
||||
export function shouldUpdateStorySummary(roundNumber: number): boolean {
|
||||
return Number.isInteger(roundNumber) && roundNumber > 0 && roundNumber % 3 === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* PostgreSQL stores memory entity references as uuid[]. Model-authored labels
|
||||
* are useful prose but cannot be cast to that column and would roll back an
|
||||
* otherwise valid round. Preserve only storage-compatible references.
|
||||
*/
|
||||
export function sanitizeRoundMemory(memory: RoundResolution['memory']): RoundResolution['memory'] {
|
||||
if (!memory) return null
|
||||
return {
|
||||
...memory,
|
||||
entityIds: [...new Set(memory.entityIds.filter(id => postgresUuidPattern.test(id)))],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user