feat(core): Added isAgent field for entities

This commit is contained in:
2026-07-15 19:19:29 +05:30
parent 3b2a85aeaf
commit 9838d4ce59
9 changed files with 408 additions and 129 deletions

View File

@@ -3,9 +3,11 @@ import { AttributableObject } from "./attribute.js";
export class Entity extends AttributableObject {
locationId: string | null = null;
readonly aliases: Map<string, string> = new Map();
isAgent: boolean = true;
constructor(id?: string, locationId?: string | null) {
constructor(id?: string, locationId?: string | null, isAgent?: boolean) {
super(id);
this.locationId = locationId ?? null;
this.isAgent = isAgent ?? true;
}
}

View File

@@ -73,6 +73,13 @@ export class SQLiteRepository {
} catch {
// Column already exists, ignore error
}
// Safely add is_agent column if it does not exist in an existing database
try {
this.db.exec("ALTER TABLE objects ADD COLUMN is_agent INTEGER;");
} catch {
// Column already exists, ignore error
}
}
save(obj: AttributableObject, type: string, worldId?: string): void {
@@ -85,15 +92,20 @@ export class SQLiteRepository {
let locationId: string | null = null;
let aliasesJson: string | null = null;
let connectionsJson: string | null = null;
let isAgent: number | null = null;
if (obj instanceof Entity) {
locationId = obj.locationId;
aliasesJson = JSON.stringify(Array.from(obj.aliases.entries()));
isAgent = obj.isAgent ? 1 : 0;
}
// Check if it's a location (using duck typing to avoid circular import of Location)
if (type === "location") {
const loc = obj as { parentId?: string | null; connections?: unknown[] };
const loc = obj as {
parentId?: string | null;
connections?: unknown[];
};
locationId = loc.parentId ?? null;
if (loc.connections) {
connectionsJson = JSON.stringify(loc.connections);
@@ -104,18 +116,28 @@ export class SQLiteRepository {
this.db
.prepare(
`
INSERT INTO objects (id, type, world_id, clock_iso, location_id, aliases_json, connections_json)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO objects (id, type, world_id, clock_iso, location_id, aliases_json, connections_json, is_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
type = excluded.type,
world_id = excluded.world_id,
clock_iso = excluded.clock_iso,
location_id = excluded.location_id,
aliases_json = excluded.aliases_json,
connections_json = excluded.connections_json
connections_json = excluded.connections_json,
is_agent = excluded.is_agent
`,
)
.run(obj.id, type, worldId || null, clockIso, locationId, aliasesJson, connectionsJson);
.run(
obj.id,
type,
worldId || null,
clockIso,
locationId,
aliasesJson,
connectionsJson,
isAgent,
);
// Get current attributes from db to delete the ones that are no longer present
const existingAttrs = this.db
@@ -208,16 +230,27 @@ export class SQLiteRepository {
const objRow = this.db
.prepare(
`
SELECT type, location_id, aliases_json FROM objects WHERE id = ?
SELECT type, location_id, aliases_json, is_agent FROM objects WHERE id = ?
`,
)
.get(id) as { type: string; location_id: string | null; aliases_json: string | null } | undefined;
.get(id) as
| {
type: string;
location_id: string | null;
aliases_json: string | null;
is_agent: number | null;
}
| undefined;
if (!objRow || objRow.type !== "entity") {
return null;
}
const entity = new Entity(id, objRow.location_id);
const entity = new Entity(
id,
objRow.location_id,
objRow.is_agent !== null ? objRow.is_agent === 1 : true,
);
if (objRow.aliases_json) {
const entries = JSON.parse(objRow.aliases_json) as [string, string][];
for (const [k, v] of entries) {
@@ -238,7 +271,13 @@ export class SQLiteRepository {
SELECT type, location_id, connections_json FROM objects WHERE id = ?
`,
)
.get(id) as { type: string; location_id: string | null; connections_json: string | null } | undefined;
.get(id) as
| {
type: string;
location_id: string | null;
connections_json: string | null;
}
| undefined;
if (!objRow || objRow.type !== "location") {
return null;
@@ -246,7 +285,9 @@ export class SQLiteRepository {
const location = factory(id, objRow.location_id);
if (objRow.connections_json) {
(location as { connections?: unknown[] }).connections = JSON.parse(objRow.connections_json);
(location as { connections?: unknown[] }).connections = JSON.parse(
objRow.connections_json,
);
}
this.reconstituteAttributes(location);
return location;
@@ -262,13 +303,19 @@ export class SQLiteRepository {
SELECT id, location_id, connections_json FROM objects WHERE type = 'location' AND world_id = ?
`,
)
.all(worldId) as { id: string; location_id: string | null; connections_json: string | null }[];
.all(worldId) as {
id: string;
location_id: string | null;
connections_json: string | null;
}[];
const locations: T[] = [];
for (const row of rows) {
const loc = factory(row.id, row.location_id);
if (row.connections_json) {
(loc as { connections?: unknown[] }).connections = JSON.parse(row.connections_json);
(loc as { connections?: unknown[] }).connections = JSON.parse(
row.connections_json,
);
}
this.reconstituteAttributes(loc);
locations.push(loc);
@@ -300,13 +347,19 @@ export class SQLiteRepository {
SELECT id, location_id, connections_json FROM objects WHERE type = 'location' AND world_id = ?
`,
)
.all(id) as { id: string; location_id: string | null; connections_json: string | null }[];
.all(id) as {
id: string;
location_id: string | null;
connections_json: string | null;
}[];
for (const row of locationRows) {
const loc = new GenericObject(row.id);
(loc as { parentId?: string | null }).parentId = row.location_id;
if (row.connections_json) {
(loc as { connections?: unknown[] }).connections = JSON.parse(row.connections_json);
(loc as { connections?: unknown[] }).connections = JSON.parse(
row.connections_json,
);
}
this.reconstituteAttributes(loc);
worldState.addLocation(loc);
@@ -316,13 +369,22 @@ export class SQLiteRepository {
const entityRows = this.db
.prepare(
`
SELECT id, location_id, aliases_json FROM objects WHERE type = 'entity' AND world_id = ?
SELECT id, location_id, aliases_json, is_agent FROM objects WHERE type = 'entity' AND world_id = ?
`,
)
.all(id) as { id: string; location_id: string | null; aliases_json: string | null }[];
.all(id) as {
id: string;
location_id: string | null;
aliases_json: string | null;
is_agent: number | null;
}[];
for (const row of entityRows) {
const entity = new Entity(row.id, row.location_id);
const entity = new Entity(
row.id,
row.location_id,
row.is_agent !== null ? row.is_agent === 1 : true,
);
if (row.aliases_json) {
const entries = JSON.parse(row.aliases_json) as [string, string][];
for (const [k, v] of entries) {
@@ -340,14 +402,22 @@ export class SQLiteRepository {
const rows = this.db
.prepare(
`
SELECT id, aliases_json FROM objects WHERE type = 'entity'
SELECT id, aliases_json, is_agent FROM objects WHERE type = 'entity'
`,
)
.all() as { id: string; aliases_json: string | null }[];
.all() as {
id: string;
aliases_json: string | null;
is_agent: number | null;
}[];
const entities: Entity[] = [];
for (const row of rows) {
const entity = new Entity(row.id);
const entity = new Entity(
row.id,
null,
row.is_agent !== null ? row.is_agent === 1 : true,
);
if (row.aliases_json) {
const entries = JSON.parse(row.aliases_json) as [string, string][];
for (const [k, v] of entries) {

View File

@@ -61,16 +61,21 @@ describe("Attribute & AttributableObject Unit Tests (Tier 1)", () => {
test("AttributableObject visibility filtering", () => {
const actor = new MockAttributable("actor");
actor.addAttribute("eyes", "blue", AttributeVisibility.PUBLIC);
actor.addAttribute("secret", "42", AttributeVisibility.PRIVATE, new Set(["friend"]));
actor.addAttribute(
"secret",
"42",
AttributeVisibility.PRIVATE,
new Set(["friend"]),
);
// Public viewer should only see public attributes
const publicAttrs = actor.getVisibleAttributesFor("stranger");
expect(publicAttrs.map(a => a.name)).toEqual(["eyes"]);
expect(publicAttrs.map((a) => a.name)).toEqual(["eyes"]);
// Authorized viewer should see both
const privateAttrs = actor.getVisibleAttributesFor("friend");
expect(privateAttrs.map(a => a.name)).toContain("eyes");
expect(privateAttrs.map(a => a.name)).toContain("secret");
expect(privateAttrs.map((a) => a.name)).toContain("eyes");
expect(privateAttrs.map((a) => a.name)).toContain("secret");
});
});
@@ -131,7 +136,12 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
const alice = new Entity("alice", "location-a");
alice.addAttribute("name", "Alice Smith", AttributeVisibility.PUBLIC);
// Secret attribute visible only to 'bob'
alice.addAttribute("diaries", "Private thoughts", AttributeVisibility.PRIVATE, new Set(["bob"]));
alice.addAttribute(
"diaries",
"Private thoughts",
AttributeVisibility.PRIVATE,
new Set(["bob"]),
);
world.addEntity(alice);
const bob = new Entity("bob");
@@ -161,7 +171,9 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
expect(loadedAlice!.locationId).toBe("location-a");
expect(loadedBob!.locationId).toBeNull();
expect(loadedAlice!.attributes.get("name")?.getValue()).toBe("Alice Smith");
expect(loadedAlice!.attributes.get("name")?.visibility).toBe(AttributeVisibility.PUBLIC);
expect(loadedAlice!.attributes.get("name")?.visibility).toBe(
AttributeVisibility.PUBLIC,
);
const diaryAttr = loadedAlice!.attributes.get("diaries")!;
expect(diaryAttr.getValue()).toBe("Private thoughts");
@@ -191,17 +203,46 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
db.close();
});
test("Save and load entity isAgent property", () => {
const db = new Database(":memory:");
const repo = new SQLiteRepository(db);
const world = new WorldState("world-xyz");
const alice = new Entity("alice", null, false);
const bob = new Entity("bob", null, true);
world.addEntity(alice);
world.addEntity(bob);
repo.saveWorldState(world);
const loadedWorld = repo.loadWorldState("world-xyz")!;
const loadedAlice = loadedWorld.getEntity("alice")!;
const loadedBob = loadedWorld.getEntity("bob")!;
expect(loadedAlice.isAgent).toBe(false);
expect(loadedBob.isAgent).toBe(true);
db.close();
});
});
describe("Serializer & serializeObjectiveWorldState Unit Tests (Tier 1)", () => {
test("serializeAttributes utility", () => {
const obj = new MockAttributable("obj-1");
obj.addAttribute("health", "100", AttributeVisibility.PUBLIC);
obj.addAttribute("secret", "key-123", AttributeVisibility.PRIVATE, new Set(["alice"]));
obj.addAttribute(
"secret",
"key-123",
AttributeVisibility.PRIVATE,
new Set(["alice"]),
);
const result = serializeAttributes(Array.from(obj.attributes.values()));
expect(result).toContain("* health: 100 (Visibility: PUBLIC)");
expect(result).toContain("* secret: key-123 (Visibility: PRIVATE) (Visible to: alice)");
expect(result).toContain(
"* secret: key-123 (Visibility: PRIVATE) (Visible to: alice)",
);
});
test("serializeObjectiveWorldState utility", () => {