diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fb283..86d61ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to **History** are listed here, newest first. +## v0.9.0 — 2026-10-11 20:57 +- **Reduced turn latency (prompt-level):** capped `recentEvents` in the turn prompt from the last 10 to the last 5 events, and added a HARD CONSTRAINT asking the reasoning model to keep its internal analysis brief before emitting the JSON. This shortens the chain-of-thought (the 170–223s turn driver) and lowers the risk of exhausting the 4000-token output budget before the answer (the original 502 failure mode). Reasoning stays enabled (`reasoning_effort: low`); the deterministic stub, the `TurnResult` contract, `max_tokens`, and `reasoning_effort` are all unchanged. + ## v0.8.0 — 2026-10-11 20:29 - **Remove a saved game (#20)** — every saved game now has a delete control: a trash button on its card in the home screen's "Saved games" section opens a confirmation dialog ("Delete this game?" showing the scenario/turn/date) with Cancel and a destructive Delete. The new `DELETE /api/games/:id` route removes the game's row from SQLite and returns `{ deleted: true }` (404 if the id is unknown). diff --git a/package.json b/package.json index 3fe59c8..08256d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "history", - "version": "0.8.0", + "version": "0.9.0", "private": true, "type": "module", "description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).", diff --git a/server/engine.test.ts b/server/engine.test.ts index a14e012..a7f55f6 100644 --- a/server/engine.test.ts +++ b/server/engine.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from 'vitest'; import type { GameState, Preset, RoundItem } from '../shared/types'; -import type { TurnResult } from '../shared/protocol'; +import { makeEvent, type TurnResult } from '../shared/protocol'; import type { LlmClient } from './llm'; import { makeStubLlmClient } from './stub'; import { applyTurnResult, + buildSystemPrompt, buildUserPrompt, isMeaningful, parseTurnResult, @@ -279,6 +280,44 @@ describe('buildUserPrompt (batched round)', () => { expect(parsed.roundInstruction).toMatch(/no action/i); expect(parsed.roundInstruction).toMatch(/own momentum/i); }); + + it('caps recentEvents at the last 5 (in order) when the state has more than 5 events', () => { + const state = baseState(); + const events = Array.from({ length: 8 }, (_, i) => + makeEvent( + { + nation: 'uk', + type: 'political', + title: `Event ${i + 1}`, + text: `Narrative ${i + 1}`, + outcome: 'neutral', + }, + i + 1, + `1939-09-0${i + 1}`, + ), + ); + state.events = events; + + const parsed = JSON.parse(buildUserPrompt(state, [])) as { + state: { recentEvents: Array<{ title: string; turn: number; date: string }> }; + }; + + expect(parsed.state.recentEvents).toHaveLength(5); + // the last 5 of the 8 input events, in order + expect(parsed.state.recentEvents[0].title).toBe(events[3].title); // 'Event 4' + expect(parsed.state.recentEvents[4].title).toBe(events[7].title); // 'Event 8' + expect(parsed.state.recentEvents.map((e) => e.title)).toEqual([ + events[3].title, + events[4].title, + events[5].title, + events[6].title, + events[7].title, + ]); + }); + + it('asks the model to keep its internal reasoning brief (concise-CoT constraint)', () => { + expect(buildSystemPrompt(preset)).toMatch(/reasoning brief|a few sentences/i); + }); }); describe('queueRoundItem', () => { diff --git a/server/engine.ts b/server/engine.ts index eba5a41..c10e596 100644 --- a/server/engine.ts +++ b/server/engine.ts @@ -59,6 +59,7 @@ export function buildSystemPrompt(preset: Preset): string { 'HARD CONSTRAINTS', '- Use only nation ids and ISO_A3 codes that exist in the provided state.', '- Prefer 1-4 events per turn; keep each text 1-3 sentences.', + '- Keep your internal reasoning brief (a few sentences) and go straight to the JSON result; do not over-deliberate or re-derive facts already present in the state.', '- Keep every stat value between 0 and 100.', '- Only change borders when war or a decisive political shift justifies it.', '- If a field is not relevant this turn, set it to an empty array.', @@ -95,7 +96,7 @@ export function buildUserPrompt(state: GameState, items: RoundItem[]): string { nations: Object.fromEntries( Object.entries(state.nations).map(([id, n]) => [id, nationForPrompt(n)]), ), - recentEvents: state.events.slice(-10).map((e) => ({ + recentEvents: state.events.slice(-5).map((e) => ({ turn: e.turn, date: e.date, nation: e.nation,