Merge pull request 'Reduce turn latency: cap recent events (10→5) + concise-reasoning constraint (v0.9.0)' (#23) from feat/turn-latency-hardening into master
CI / build + unit + e2e (push) Successful in 2m16s
CI / build + unit + e2e (push) Successful in 2m16s
This commit was merged in pull request #23.
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
All notable changes to **History** are listed here, newest first.
|
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
|
## 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).
|
- **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).
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "history",
|
"name": "history",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).",
|
"description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).",
|
||||||
|
|||||||
+40
-1
@@ -1,10 +1,11 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { GameState, Preset, RoundItem } from '../shared/types';
|
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 type { LlmClient } from './llm';
|
||||||
import { makeStubLlmClient } from './stub';
|
import { makeStubLlmClient } from './stub';
|
||||||
import {
|
import {
|
||||||
applyTurnResult,
|
applyTurnResult,
|
||||||
|
buildSystemPrompt,
|
||||||
buildUserPrompt,
|
buildUserPrompt,
|
||||||
isMeaningful,
|
isMeaningful,
|
||||||
parseTurnResult,
|
parseTurnResult,
|
||||||
@@ -279,6 +280,44 @@ describe('buildUserPrompt (batched round)', () => {
|
|||||||
expect(parsed.roundInstruction).toMatch(/no action/i);
|
expect(parsed.roundInstruction).toMatch(/no action/i);
|
||||||
expect(parsed.roundInstruction).toMatch(/own momentum/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', () => {
|
describe('queueRoundItem', () => {
|
||||||
|
|||||||
+2
-1
@@ -59,6 +59,7 @@ export function buildSystemPrompt(preset: Preset): string {
|
|||||||
'HARD CONSTRAINTS',
|
'HARD CONSTRAINTS',
|
||||||
'- Use only nation ids and ISO_A3 codes that exist in the provided state.',
|
'- 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.',
|
'- 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.',
|
'- Keep every stat value between 0 and 100.',
|
||||||
'- Only change borders when war or a decisive political shift justifies it.',
|
'- 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.',
|
'- 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(
|
nations: Object.fromEntries(
|
||||||
Object.entries(state.nations).map(([id, n]) => [id, nationForPrompt(n)]),
|
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,
|
turn: e.turn,
|
||||||
date: e.date,
|
date: e.date,
|
||||||
nation: e.nation,
|
nation: e.nation,
|
||||||
|
|||||||
Reference in New Issue
Block a user