feat(memory): implement stability stage
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -2,7 +2,10 @@ import {
|
||||
RoundContextSchema,
|
||||
RoundPlanSchema,
|
||||
type Character,
|
||||
type ContextEntity,
|
||||
type PlayerIntent,
|
||||
type QuestState,
|
||||
type RelationshipState,
|
||||
type RoundContext,
|
||||
type RoundPlan,
|
||||
type RoundResolution,
|
||||
@@ -17,6 +20,17 @@ export interface RoundContextInput {
|
||||
intents: PlayerIntent[]
|
||||
characters: Character[]
|
||||
memories: Array<{ summary: string; importance?: number; tags?: string[]; entityIds?: string[] }>
|
||||
entities?: ContextEntity[]
|
||||
relationships?: RelationshipState[]
|
||||
activeGoals?: QuestState[]
|
||||
}
|
||||
|
||||
export function buildRetrievalText(input: Pick<RoundContextInput, 'scene' | 'intents' | 'characters'>): string {
|
||||
return [
|
||||
input.scene,
|
||||
...input.intents.filter(intent => intent.ready).map(intent => intent.action),
|
||||
...input.characters.flatMap(character => [character.name, character.concept]),
|
||||
].join(' ').replace(/\s+/g, ' ').trim().slice(0, 4_000)
|
||||
}
|
||||
|
||||
export function buildRoundContext(input: RoundContextInput): RoundContext {
|
||||
@@ -51,6 +65,9 @@ export function buildRoundContext(input: RoundContextInput): RoundContext {
|
||||
humanIntents,
|
||||
aiCharacters,
|
||||
activeCharacters: input.characters,
|
||||
entities: input.entities ?? [],
|
||||
relationships: input.relationships ?? [],
|
||||
activeGoals: input.activeGoals ?? [],
|
||||
memories: rankedMemories,
|
||||
})
|
||||
}
|
||||
@@ -58,6 +75,9 @@ export function buildRoundContext(input: RoundContextInput): RoundContext {
|
||||
export function validateAndOrderRoundPlan(planInput: unknown, context: RoundContext): RoundPlan {
|
||||
const plan = RoundPlanSchema.parse(planInput)
|
||||
const characterIds = new Set(context.activeCharacters.map(character => character.id))
|
||||
const entityIds = new Set(context.entities.map(entity => entity.id))
|
||||
const worldStateIds = new Set([...characterIds, ...entityIds])
|
||||
const entityKinds = new Map(context.entities.map(entity => [entity.id, entity.kind]))
|
||||
const aiOrder = new Map(context.aiCharacters.map((character, index) => [character.id, index]))
|
||||
const seenAiActors = new Set<string>()
|
||||
|
||||
@@ -74,14 +94,26 @@ export function validateAndOrderRoundPlan(planInput: unknown, context: RoundCont
|
||||
if (['damage', 'healing'].includes(check.kind) && !check.targetId) throw new Error(`${check.kind} rolls require a target`)
|
||||
}
|
||||
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}`)
|
||||
const mechanical = ['damage', 'healing', 'inventory', 'status', 'temporaryHp', 'resource', 'rest'].includes(event.type)
|
||||
if (event.actorId && !(mechanical ? characterIds : worldStateIds).has(event.actorId)) throw new Error(`Unknown event actor ${event.actorId}`)
|
||||
if (event.targetId && !(mechanical ? characterIds : worldStateIds).has(event.targetId)) throw new Error(`Unknown event target ${event.targetId}`)
|
||||
if (['damage', 'healing', 'inventory', 'status', 'temporaryHp', 'resource', 'rest'].includes(event.type) && !event.targetId) {
|
||||
throw new Error(`${event.type} event requires a target`)
|
||||
}
|
||||
if (event.type === 'damage' && !event.damageType) throw new Error('damage events require a damage type')
|
||||
if (event.type === 'resource' && !event.item) throw new Error('resource events require a resource name')
|
||||
if (event.type === 'rest' && !['short', 'long'].includes(String(event.item))) throw new Error('rest events require short or long')
|
||||
if (event.type === 'relationship') {
|
||||
if (!event.actorId || !event.targetId || event.actorId === event.targetId) throw new Error('relationship events require two distinct known entities')
|
||||
if (event.value === null || event.value < -20 || event.value > 20) throw new Error('relationship changes must be between -20 and 20')
|
||||
}
|
||||
if (event.type === 'quest') {
|
||||
if (!event.targetId || entityKinds.get(event.targetId) !== 'quest') throw new Error('quest events require a known quest entity')
|
||||
if (!['hidden', 'active', 'completed', 'failed'].includes(String(event.item))) throw new Error('quest events require a valid quest status')
|
||||
}
|
||||
if (event.type === 'narrative' && event.item === 'scene' && event.targetId && entityKinds.get(event.targetId) !== 'location') {
|
||||
throw new Error('scene events may only target a known location')
|
||||
}
|
||||
}
|
||||
|
||||
const aiActions = plan.aiActions
|
||||
@@ -112,10 +144,12 @@ export function shouldUpdateStorySummary(roundNumber: number): boolean {
|
||||
* 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'] {
|
||||
export function sanitizeRoundMemory(memory: RoundResolution['memory'], allowedEntityIds?: ReadonlySet<string>): RoundResolution['memory'] {
|
||||
if (!memory) return null
|
||||
return {
|
||||
...memory,
|
||||
entityIds: [...new Set(memory.entityIds.filter(id => postgresUuidPattern.test(id)))],
|
||||
entityIds: [...new Set(memory.entityIds.filter(id =>
|
||||
postgresUuidPattern.test(id) && (!allowedEntityIds || allowedEntityIds.has(id)),
|
||||
))],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user