From e38cc5933f95c13f98d64dde749c3bae6896f7b0 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Sun, 11 Oct 2026 20:59:18 +0000 Subject: [PATCH] =?UTF-8?q?Reduce=20turn=20latency:=20cap=20recent=20event?= =?UTF-8?q?s=20(10=E2=86=925)=20+=20concise-reasoning=20constraint=20(v0.9?= =?UTF-8?q?.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #22 --- CHANGELOG.md | 3 +++ package.json | 2 +- server/engine.test.ts | 41 ++++++++++++++++++++++++++++++++++++++++- server/engine.ts | 3 ++- 4 files changed, 46 insertions(+), 3 deletions(-) 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,