diff --git a/src/lib/assistant/prompts.test.ts b/src/lib/assistant/prompts.test.ts index 96af236..f7a2f60 100644 --- a/src/lib/assistant/prompts.test.ts +++ b/src/lib/assistant/prompts.test.ts @@ -49,4 +49,17 @@ describe('balanceSuggestionSchema', () => { expect(r.success).toBe(true); if (r.success) expect(r.data.add[0]!.count).toBe(2); }); + it('accepts the "creatures" alias some models use for the add array', () => { + // Exact shape DeepSeek returned in the wild — array keyed "creatures", not "add". + const r = balanceSuggestionSchema.safeParse({ + creatures: [{ name: 'Berserker', count: 1 }], + reasoning: 'Adding one CR 2 Berserker makes this deadly for a lone level 1 paladin.', + targetDifficulty: 'deadly', + }); + expect(r.success).toBe(true); + if (r.success) { + expect(r.data.add).toHaveLength(1); + expect(r.data.add[0]!.name).toBe('Berserker'); + } + }); }); diff --git a/src/lib/assistant/prompts.ts b/src/lib/assistant/prompts.ts index bca4048..4a3d0f1 100644 --- a/src/lib/assistant/prompts.ts +++ b/src/lib/assistant/prompts.ts @@ -7,7 +7,23 @@ import type { CampaignContext, CreatureCandidate } from './context'; // over-eager suggestion is accepted rather than rejecting the whole response. const monsterCount = z.coerce.number().int().min(1).max(12); -export const balanceSuggestionSchema = z.object({ +/** + * Models sometimes name the additions array "creatures"/"monsters" instead of the + * expected "add" (DeepSeek does this). Normalize those synonyms before validation + * so a correct suggestion isn't discarded over a key-name mismatch. + */ +function normalizeBalanceShape(raw: unknown): unknown { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return raw; + const o = { ...(raw as Record) }; + if (o.add === undefined) { + for (const alias of ['creatures', 'monsters', 'additions', 'addCreatures', 'add_creatures']) { + if (Array.isArray(o[alias])) { o.add = o[alias]; break; } + } + } + return o; +} + +export const balanceSuggestionSchema = z.preprocess(normalizeBalanceShape, z.object({ reasoning: z.string(), add: z.array(z.object({ name: z.string(), @@ -20,7 +36,7 @@ export const balanceSuggestionSchema = z.object({ count: monsterCount, })).optional(), targetDifficulty: z.string(), -}); +})); export type BalanceSuggestion = z.infer; function partyLine(ctx: CampaignContext): string { @@ -38,10 +54,14 @@ export function buildBalancePrompt( const system = `${ctx.systemConstraint}\n\n` + `You are an encounter-balancing assistant for a ${ctx.systemLabel} game master. ` + - `Recommend which creatures to ADD to the current encounter to bring it to roughly "${current.targetDifficulty}" difficulty. ` + + `Recommend which creatures to add to the current encounter to bring it to roughly "${current.targetDifficulty}" difficulty. ` + `You MUST choose only from the provided candidate creatures (by exact name). Do not invent creatures or use any from another system. ` + - `Prefer 1–3 distinct creatures with sensible counts. Keep the reasoning to one or two sentences. ` + - `Each "count" must be a plain integer (e.g. 2, not "2"). Always include "reasoning" and "targetDifficulty".`; + `Prefer 1–3 distinct creatures with sensible counts.\n\n` + + `Respond with a JSON object with EXACTLY these keys:\n` + + ` "add": array of objects, each { "name": , "count": }\n` + + ` "reasoning": a one or two sentence string\n` + + ` "targetDifficulty": the target difficulty as a string\n` + + `Name the array "add" — not "creatures" or "monsters". Counts are plain integers (2, not "2").`; const candidateList = current.candidates .map((c) => `- ${c.name} (rating ${c.rating}${c.hp ? `, hp ${c.hp}` : ''}${c.ac ? `, ac ${c.ac}` : ''})`) @@ -52,7 +72,7 @@ export function buildBalancePrompt( `Current encounter difficulty: ${current.difficulty}.\n` + `Target difficulty: ${current.targetDifficulty}.\n\n` + `Candidate creatures (choose only from these):\n${candidateList}\n\n` + - `Respond with the creatures to add (name + count), a short reasoning, and the targetDifficulty.`; + `Return the JSON object described above (keys: "add", "reasoning", "targetDifficulty").`; return { system, user }; }