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:
@@ -2,7 +2,11 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
import { Action, Belief, Entity, GameEvent, Summary, Turn } from "./types";
|
||||
import type { Action } from "./contracts/action";
|
||||
import type { Entity } from "./contracts/entity";
|
||||
import type { Turn } from "./contracts/turn";
|
||||
import type { ValidationResult } from "./contracts/validation";
|
||||
import type { WorldState } from "./contracts/world";
|
||||
|
||||
export interface DatabaseConfig {
|
||||
dbPath: string;
|
||||
@@ -12,58 +16,19 @@ export interface CharacterGardenDatabase {
|
||||
sqlite: Database.Database;
|
||||
init(): void;
|
||||
close(): void;
|
||||
upsertEntity(entity: Entity): void;
|
||||
upsertEntities(entities: Entity[]): void;
|
||||
listEntities(): Entity[];
|
||||
insertEvent(event: GameEvent): void;
|
||||
listEvents(): GameEvent[];
|
||||
insertTurn(turn: Turn): void;
|
||||
listTurns(): Turn[];
|
||||
insertBelief(belief: Belief): void;
|
||||
listBeliefs(entityId?: string): Belief[];
|
||||
insertSummary(summary: Summary): void;
|
||||
listSummaries(): Summary[];
|
||||
insertActions(turnId: string, actions: Action[]): void;
|
||||
insertValidationResults(turnId: string, results: ValidationResult[]): void;
|
||||
insertWorldState(turnId: string | null, worldState: WorldState): void;
|
||||
getLatestWorldState(): WorldState | null;
|
||||
wipe(): void;
|
||||
}
|
||||
|
||||
type EntityRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
attributes_json: string;
|
||||
};
|
||||
|
||||
type EventRow = {
|
||||
id: string;
|
||||
turn: number;
|
||||
action_json: string;
|
||||
result: "success" | "fail";
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type TurnRow = {
|
||||
id: string;
|
||||
turn: number;
|
||||
input: string;
|
||||
output: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type BeliefRow = {
|
||||
entity_id: string;
|
||||
claim: string;
|
||||
confidence: number;
|
||||
};
|
||||
|
||||
type SummaryRow = {
|
||||
id: string;
|
||||
turn_start: number;
|
||||
turn_end: number;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
function ensureParentDirectory(dbPath: string): void {
|
||||
const directory = path.dirname(dbPath);
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
}
|
||||
|
||||
function parseJson<T>(value: string): T {
|
||||
@@ -72,51 +37,55 @@ function parseJson<T>(value: string): T {
|
||||
|
||||
export function createDatabase(config: DatabaseConfig): CharacterGardenDatabase {
|
||||
ensureParentDirectory(config.dbPath);
|
||||
|
||||
const sqlite = new Database(config.dbPath);
|
||||
|
||||
const initStatements = [
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS turns (
|
||||
id TEXT PRIMARY KEY,
|
||||
raw_text TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS actions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
turn_id TEXT NOT NULL,
|
||||
action_index INTEGER NOT NULL,
|
||||
actor_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
target_id TEXT,
|
||||
location_id TEXT,
|
||||
metadata_json TEXT,
|
||||
FOREIGN KEY(turn_id) REFERENCES turns(id)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS validation_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
turn_id TEXT NOT NULL,
|
||||
action_index INTEGER NOT NULL,
|
||||
success INTEGER NOT NULL,
|
||||
reason TEXT,
|
||||
message TEXT,
|
||||
FOREIGN KEY(turn_id) REFERENCES turns(id)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
attributes_json TEXT NOT NULL
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
CREATE TABLE IF NOT EXISTS world_states (
|
||||
id TEXT PRIMARY KEY,
|
||||
turn INTEGER NOT NULL,
|
||||
action_json TEXT NOT NULL,
|
||||
result TEXT NOT NULL CHECK(result IN ('success', 'fail')),
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS turns (
|
||||
id TEXT PRIMARY KEY,
|
||||
turn INTEGER NOT NULL UNIQUE,
|
||||
input TEXT NOT NULL,
|
||||
output TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS beliefs (
|
||||
entity_id TEXT NOT NULL,
|
||||
claim TEXT NOT NULL,
|
||||
confidence REAL NOT NULL,
|
||||
PRIMARY KEY (entity_id, claim)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS summaries (
|
||||
id TEXT PRIMARY KEY,
|
||||
turn_start INTEGER NOT NULL,
|
||||
turn_end INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
turn_id TEXT,
|
||||
state_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(turn_id) REFERENCES turns(id)
|
||||
)
|
||||
`,
|
||||
];
|
||||
@@ -125,163 +94,207 @@ export function createDatabase(config: DatabaseConfig): CharacterGardenDatabase
|
||||
sqlite.exec(statement);
|
||||
}
|
||||
|
||||
const clearEntitiesStatement = sqlite.prepare("DELETE FROM entities");
|
||||
const upsertEntityStatement = sqlite.prepare(`
|
||||
INSERT INTO entities (id, type, name, attributes_json)
|
||||
VALUES (@id, @type, @name, @attributes_json)
|
||||
INSERT INTO entities (id, name, type, attributes_json)
|
||||
VALUES (@id, @name, @type, @attributes_json)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
type = excluded.type,
|
||||
name = excluded.name,
|
||||
type = excluded.type,
|
||||
attributes_json = excluded.attributes_json
|
||||
`);
|
||||
|
||||
const listEntitiesStatement = sqlite.prepare(`
|
||||
SELECT id, type, name, attributes_json
|
||||
SELECT id, name, type, attributes_json
|
||||
FROM entities
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
|
||||
const insertEventStatement = sqlite.prepare(`
|
||||
INSERT INTO events (id, turn, action_json, result, timestamp)
|
||||
VALUES (@id, @turn, @action_json, @result, @timestamp)
|
||||
`);
|
||||
|
||||
const listEventsStatement = sqlite.prepare(`
|
||||
SELECT id, turn, action_json, result, timestamp
|
||||
FROM events
|
||||
ORDER BY turn ASC, timestamp ASC, id ASC
|
||||
`);
|
||||
|
||||
const insertTurnStatement = sqlite.prepare(`
|
||||
INSERT INTO turns (id, turn, input, output, timestamp)
|
||||
VALUES (@id, @turn, @input, @output, @timestamp)
|
||||
INSERT INTO turns (id, raw_text, created_at)
|
||||
VALUES (@id, @raw_text, @created_at)
|
||||
`);
|
||||
|
||||
const listTurnsStatement = sqlite.prepare(`
|
||||
SELECT id, turn, input, output, timestamp
|
||||
SELECT id, raw_text, created_at
|
||||
FROM turns
|
||||
ORDER BY turn ASC
|
||||
ORDER BY created_at ASC
|
||||
`);
|
||||
|
||||
const insertBeliefStatement = sqlite.prepare(`
|
||||
INSERT INTO beliefs (entity_id, claim, confidence)
|
||||
VALUES (@entity_id, @claim, @confidence)
|
||||
ON CONFLICT(entity_id, claim) DO UPDATE SET
|
||||
confidence = excluded.confidence
|
||||
const insertActionStatement = sqlite.prepare(`
|
||||
INSERT INTO actions (
|
||||
turn_id,
|
||||
action_index,
|
||||
actor_id,
|
||||
type,
|
||||
target_id,
|
||||
location_id,
|
||||
metadata_json
|
||||
) VALUES (
|
||||
@turn_id,
|
||||
@action_index,
|
||||
@actor_id,
|
||||
@type,
|
||||
@target_id,
|
||||
@location_id,
|
||||
@metadata_json
|
||||
)
|
||||
`);
|
||||
|
||||
const listBeliefsStatement = sqlite.prepare(`
|
||||
SELECT entity_id, claim, confidence
|
||||
FROM beliefs
|
||||
ORDER BY entity_id ASC, claim ASC
|
||||
const insertValidationStatement = sqlite.prepare(`
|
||||
INSERT INTO validation_results (
|
||||
turn_id,
|
||||
action_index,
|
||||
success,
|
||||
reason,
|
||||
message
|
||||
) VALUES (
|
||||
@turn_id,
|
||||
@action_index,
|
||||
@success,
|
||||
@reason,
|
||||
@message
|
||||
)
|
||||
`);
|
||||
|
||||
const listBeliefsByEntityStatement = sqlite.prepare(`
|
||||
SELECT entity_id, claim, confidence
|
||||
FROM beliefs
|
||||
WHERE entity_id = ?
|
||||
ORDER BY claim ASC
|
||||
const insertWorldStateStatement = sqlite.prepare(`
|
||||
INSERT INTO world_states (id, turn_id, state_json, created_at)
|
||||
VALUES (@id, @turn_id, @state_json, @created_at)
|
||||
`);
|
||||
|
||||
const insertSummaryStatement = sqlite.prepare(`
|
||||
INSERT INTO summaries (id, turn_start, turn_end, text, timestamp)
|
||||
VALUES (@id, @turn_start, @turn_end, @text, @timestamp)
|
||||
`);
|
||||
|
||||
const listSummariesStatement = sqlite.prepare(`
|
||||
SELECT id, turn_start, turn_end, text, timestamp
|
||||
FROM summaries
|
||||
ORDER BY turn_start ASC, turn_end ASC
|
||||
const latestWorldStateStatement = sqlite.prepare(`
|
||||
SELECT state_json
|
||||
FROM world_states
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return {
|
||||
sqlite,
|
||||
|
||||
init() {
|
||||
// Schema is applied on database construction so prepared statements are valid.
|
||||
// Tables are initialized on construction.
|
||||
},
|
||||
|
||||
close() {
|
||||
sqlite.close();
|
||||
},
|
||||
|
||||
upsertEntity(entity) {
|
||||
upsertEntityStatement.run({
|
||||
id: entity.id,
|
||||
type: entity.type,
|
||||
name: entity.name,
|
||||
attributes_json: JSON.stringify(entity.attributes),
|
||||
wipe() {
|
||||
sqlite.exec(`
|
||||
DELETE FROM validation_results;
|
||||
DELETE FROM actions;
|
||||
DELETE FROM world_states;
|
||||
DELETE FROM turns;
|
||||
DELETE FROM entities;
|
||||
`);
|
||||
},
|
||||
|
||||
upsertEntities(entities) {
|
||||
const tx = sqlite.transaction((entityList: Entity[]) => {
|
||||
clearEntitiesStatement.run();
|
||||
for (const entity of entityList) {
|
||||
upsertEntityStatement.run({
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
type: entity.type,
|
||||
attributes_json: JSON.stringify(entity.attributes),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tx(entities);
|
||||
},
|
||||
|
||||
listEntities() {
|
||||
const rows = listEntitiesStatement.all() as EntityRow[];
|
||||
const rows = listEntitiesStatement.all() as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
attributes_json: string;
|
||||
}>;
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
attributes: parseJson<Record<string, unknown>>(row.attributes_json),
|
||||
}));
|
||||
},
|
||||
|
||||
insertEvent(event) {
|
||||
insertEventStatement.run({
|
||||
id: event.id,
|
||||
turn: event.turn,
|
||||
action_json: JSON.stringify(event.action),
|
||||
result: event.result,
|
||||
timestamp: event.timestamp,
|
||||
});
|
||||
},
|
||||
|
||||
listEvents() {
|
||||
const rows = listEventsStatement.all() as EventRow[];
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
turn: row.turn,
|
||||
action: parseJson<Action>(row.action_json),
|
||||
result: row.result,
|
||||
timestamp: row.timestamp,
|
||||
}));
|
||||
},
|
||||
|
||||
insertTurn(turn) {
|
||||
insertTurnStatement.run(turn);
|
||||
insertTurnStatement.run({
|
||||
id: turn.id,
|
||||
raw_text: turn.rawText,
|
||||
created_at: turn.createdAt,
|
||||
});
|
||||
},
|
||||
|
||||
listTurns() {
|
||||
return listTurnsStatement.all() as TurnRow[];
|
||||
const rows = listTurnsStatement.all() as Array<{
|
||||
id: string;
|
||||
raw_text: string;
|
||||
created_at: number;
|
||||
}>;
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
rawText: row.raw_text,
|
||||
actions: [],
|
||||
validation: [],
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
},
|
||||
|
||||
insertBelief(belief) {
|
||||
insertBeliefStatement.run(belief);
|
||||
insertActions(turnId, actions) {
|
||||
const tx = sqlite.transaction((actionList: Action[]) => {
|
||||
actionList.forEach((action, index) => {
|
||||
insertActionStatement.run({
|
||||
turn_id: turnId,
|
||||
action_index: index,
|
||||
actor_id: action.actorId,
|
||||
type: action.type,
|
||||
target_id: action.targetId ?? null,
|
||||
location_id: action.locationId ?? null,
|
||||
metadata_json: action.metadata ? JSON.stringify(action.metadata) : null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tx(actions);
|
||||
},
|
||||
|
||||
listBeliefs(entityId) {
|
||||
if (entityId) {
|
||||
return listBeliefsByEntityStatement.all(entityId) as BeliefRow[];
|
||||
}
|
||||
insertValidationResults(turnId, results) {
|
||||
const tx = sqlite.transaction((validationList: ValidationResult[]) => {
|
||||
for (const result of validationList) {
|
||||
insertValidationStatement.run({
|
||||
turn_id: turnId,
|
||||
action_index: result.actionIndex,
|
||||
success: result.success ? 1 : 0,
|
||||
reason: result.reason ?? null,
|
||||
message: result.message ?? null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return listBeliefsStatement.all() as BeliefRow[];
|
||||
tx(results);
|
||||
},
|
||||
|
||||
insertSummary(summary) {
|
||||
insertSummaryStatement.run({
|
||||
id: summary.id,
|
||||
turn_start: summary.turn_range[0],
|
||||
turn_end: summary.turn_range[1],
|
||||
text: summary.text,
|
||||
timestamp: summary.timestamp,
|
||||
insertWorldState(turnId, worldState) {
|
||||
insertWorldStateStatement.run({
|
||||
id: worldState.id,
|
||||
turn_id: turnId,
|
||||
state_json: JSON.stringify(worldState),
|
||||
created_at: worldState.createdAt,
|
||||
});
|
||||
},
|
||||
|
||||
listSummaries() {
|
||||
const rows = listSummariesStatement.all() as SummaryRow[];
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
turn_range: [row.turn_start, row.turn_end],
|
||||
text: row.text,
|
||||
timestamp: row.timestamp,
|
||||
}));
|
||||
getLatestWorldState() {
|
||||
const row = latestWorldStateStatement.get() as { state_json: string } | undefined;
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return parseJson<WorldState>(row.state_json);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user