feat: implement processTurn function to handle turn processing and world state updates

refactor: remove legacy types.ts file and update frontend to use new contracts

feat: add applyActions function to manage action application and world state mutation

chore: remove empty .gitkeep file from sqlite data directory

refactor: update frontend App component to align with new API contracts and improve UX

docs: revise project.md to reflect updated architecture and system requirements

docs: update thoughts.md with current status, architecture decisions, and remaining checks
This commit is contained in:
2026-04-24 01:04:17 -04:00
parent 2f6af46c79
commit 998635f542
21 changed files with 1472 additions and 1740 deletions

View File

@@ -7,60 +7,52 @@ type Entity = {
attributes: Record<string, unknown>;
};
type GameEvent = {
id: string;
turn: number;
result: "success" | "fail";
action: Record<string, unknown>;
timestamp: number;
type Action = {
actorId: string;
type: string;
targetId?: string;
locationId?: string;
};
type ValidationResult = {
actionIndex: number;
success: boolean;
reason?: string;
message?: string;
};
type Turn = {
id: string;
turn: number;
input: string;
output: string;
timestamp: number;
rawText: string;
actions: Action[];
validation: ValidationResult[];
createdAt: number;
};
type Belief = {
entity_id: string;
claim: string;
confidence: number;
};
type Summary = {
type WorldState = {
id: string;
turn_range: [number, number];
text: string;
timestamp: number;
entities: Record<string, Entity>;
metadata: Record<string, unknown>;
createdAt: number;
};
type Snapshot = {
entities: Entity[];
events: GameEvent[];
type AppSnapshot = {
worldState: WorldState;
turns: Turn[];
beliefs: Belief[];
summaries: Summary[];
};
type TurnResult = {
narration: string;
parser: string;
parser_feedback?: string;
actions: Array<Record<string, unknown>>;
accepted: Array<Record<string, unknown>>;
rejected: Array<{ action: Record<string, unknown>; reason: string }>;
latent_resolution?: { accepted: boolean; reason: string; entity_id?: string };
snapshot: Snapshot;
type ProcessTurnResponse = {
rawText: string;
actions: Action[];
validation: ValidationResult[];
worldState: WorldState;
};
const starterPrompts = [
"look around",
"open the gate",
"talk to the groundskeeper",
"go to the shed",
"pull out my phone",
"take key",
"open door",
"move to exit",
];
async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
@@ -72,15 +64,15 @@ async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T>
}
export default function App() {
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
const [latest, setLatest] = useState<TurnResult | null>(null);
const [snapshot, setSnapshot] = useState<AppSnapshot | null>(null);
const [latest, setLatest] = useState<ProcessTurnResponse | null>(null);
const [input, setInput] = useState("look around");
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
void fetchJson<Snapshot>("/api/state")
void fetchJson<AppSnapshot>("/api/state")
.then((data) => {
setSnapshot(data);
setLoading(false);
@@ -97,13 +89,14 @@ export default function App() {
setError(null);
try {
const result = await fetchJson<TurnResult>("/api/turn", {
const result = await fetchJson<ProcessTurnResponse>("/api/turn", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input }),
});
setLatest(result);
setSnapshot(result.snapshot);
const nextSnapshot = await fetchJson<AppSnapshot>("/api/state");
setSnapshot(nextSnapshot);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unknown error");
} finally {
@@ -111,13 +104,30 @@ export default function App() {
}
}
async function onReset() {
setError(null);
try {
const result = await fetchJson<AppSnapshot>("/api/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
setSnapshot(result);
setLatest(null);
} catch (resetError) {
setError(resetError instanceof Error ? resetError.message : "Unknown error");
}
}
const entities = snapshot ? Object.values(snapshot.worldState.entities) : [];
return (
<main className="page-shell">
<section className="hero-panel">
<p className="eyebrow">CharacterGarden</p>
<h1>Bootable narrative sandbox</h1>
<p className="lede">
Submit a turn, inspect the current entities and events, and verify how the truth engine is mutating state.
Submit a turn, inspect world state, and verify how the truth engine is mutating state.
</p>
<form className="turn-form" onSubmit={onSubmit}>
@@ -133,6 +143,9 @@ export default function App() {
<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Run turn"}
</button>
<button type="button" className="chip" onClick={onReset}>
Reset world
</button>
<div className="chips">
{starterPrompts.map((prompt) => (
<button key={prompt} type="button" className="chip" onClick={() => setInput(prompt)}>
@@ -146,11 +159,16 @@ export default function App() {
{latest ? (
<section className="result-card">
<h2>Latest result</h2>
<p>{latest.narration}</p>
{latest.parser_feedback ? (
<p className="parser-hint">Parser guidance: {latest.parser_feedback}</p>
) : null}
<pre>{JSON.stringify({ actions: latest.actions, rejected: latest.rejected, latent: latest.latent_resolution }, null, 0)}</pre>
<p><strong>Input:</strong> {latest.rawText}</p>
<ul className="timeline-list compact">
{latest.validation.map((v) => (
<li key={v.actionIndex}>
Action {v.actionIndex}: {v.success ? "ok" : `failed (${v.reason ?? "unknown"})`}
{v.message ? ` - ${v.message}` : ""}
</li>
))}
{latest.validation.length === 0 ? <li>No actions parsed.</li> : null}
</ul>
</section>
) : null}
@@ -162,7 +180,7 @@ export default function App() {
<h2>World state</h2>
{loading && !snapshot ? <p>Loading...</p> : null}
<ul className="entity-list">
{snapshot?.entities.map((entity) => (
{entities.map((entity) => (
<li key={entity.id}>
<strong>{entity.name}</strong> <span>{entity.type}</span>
<pre>{JSON.stringify(entity.attributes, null, 0)}</pre>
@@ -176,38 +194,10 @@ export default function App() {
<ul className="timeline-list">
{snapshot?.turns.slice().reverse().map((turn) => (
<li key={turn.id}>
<strong>Turn {turn.turn}:</strong> {turn.input} {turn.output}
</li>
))}
</ul>
</article>
<article className="panel">
<h2>Events</h2>
<ul className="timeline-list compact">
{snapshot?.events.slice().reverse().map((event) => (
<li key={event.id}>
<strong>{event.result}:</strong> <pre>{JSON.stringify(event.action, null, 0)}</pre>
</li>
))}
</ul>
</article>
<article className="panel">
<h2>Beliefs & summaries</h2>
<h3>Beliefs</h3>
<ul className="timeline-list compact">
{snapshot?.beliefs.map((belief) => (
<li key={`${belief.entity_id}-${belief.claim}`}>
<strong>{belief.entity_id}:</strong> {belief.claim} ({belief.confidence})
</li>
))}
</ul>
<h3>Summaries</h3>
<ul className="timeline-list compact">
{snapshot?.summaries.map((summary) => (
<li key={summary.id}>
<strong>{summary.turn_range.join("-")}:</strong> {summary.text}
<strong>{turn.rawText}</strong>
{turn.validation.map((v) => (
<span key={v.actionIndex}> [{v.success ? "ok" : v.reason}]</span>
))}
</li>
))}
</ul>