@agent-os-lab/agent-game-sdk 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,7 @@ const view = await mountAgentGameOffice(container, {
33
33
  });
34
34
 
35
35
  view.focusAgent("agent-1");
36
+ view.resetCamera();
36
37
  view.refreshAgents();
37
38
  view.destroy();
38
39
  ```
@@ -57,6 +58,9 @@ export function Office() {
57
58
  <button type="button" onClick={() => view?.refreshAgents()}>
58
59
  Refresh agents
59
60
  </button>
61
+ <button type="button" onClick={() => view?.resetCamera()}>
62
+ Reset view
63
+ </button>
60
64
  <AgentGameOfficeView
61
65
  renderer="three"
62
66
  office={{
package/USAGE.md CHANGED
@@ -203,6 +203,7 @@ const view = await mountAgentGameOffice(container, {
203
203
  });
204
204
 
205
205
  view.focusAgent("agent-1");
206
+ view.resetCamera();
206
207
  view.refreshAgents();
207
208
  view.destroy();
208
209
  ```
@@ -221,6 +222,8 @@ The layout packs up to three rooms per row, then starts another row. `capacity`
221
222
 
222
223
  The SDK keeps runtime statuses simple. `idle` and `resting` agents are locally distributed across ambient anchors for lounge, pantry, gym, and reading areas. `entertaining` stays biased toward the review area. If a configured room does not provide the selected ambient zone, the SDK falls back to another available non-offstage anchor.
223
224
 
225
+ `resetCamera` returns the office view to its initial camera position, rotation, target, zoom, and follow state. It is intended for UI controls such as a reset-view button after a user pans, rotates, zooms, or follows an Agent.
226
+
224
227
  `refreshAgents` asks Agent Game Runtime to refresh the tenant roster through the existing bootstrap flow. It is intended for UI controls such as a refresh button after an Agent was created or deleted in another surface.
225
228
 
226
229
  ## React Office View
@@ -245,6 +248,9 @@ export function Office() {
245
248
  <button type="button" onClick={() => view?.refreshAgents()}>
246
249
  Refresh agents
247
250
  </button>
251
+ <button type="button" onClick={() => view?.resetCamera()}>
252
+ Reset view
253
+ </button>
248
254
  <AgentGameOfficeView
249
255
  renderer="three"
250
256
  office={{
@@ -308,6 +314,8 @@ view.updateAgents([
308
314
 
309
315
  `updateAgents` only mutates SDK-managed snapshot sources. Runtime and custom sources own their own updates.
310
316
 
317
+ `resetCamera` is source-independent and can be called for snapshot, runtime, and custom sources.
318
+
311
319
  ## Custom Source
312
320
 
313
321
  Use `source.type: "custom"` when another state manager already projects runtime state into office snapshots.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-os-lab/agent-game-sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./runtime-client";
2
2
  export * from "./runtime-agent-list";
3
3
  export * from "./office";
4
+ export * from "./graph";
@@ -87,10 +87,24 @@ export type AgentGameOfficeSourceInput =
87
87
 
88
88
  export type AgentGameOfficeRendererController = {
89
89
  update(snapshot: AgentGameOfficeSnapshot): void;
90
+ resetCamera?(): void;
90
91
  focusAgent?(agentId: string | null): void;
92
+ focusFloor?(floorId: string | null): void;
93
+ getFocusedFloor?(): string | null;
94
+ getNavigationErrors?(): AgentNavigationError[];
91
95
  destroy(): void;
92
96
  };
93
97
 
98
+ export type AgentNavigationError = {
99
+ agentId: string;
100
+ code: "missing-zone-anchor" | "missing-route" | "stale-location-node";
101
+ message: string;
102
+ zoneId?: AgentGameOfficeZoneId;
103
+ fromNodeId?: string;
104
+ toNodeId?: string;
105
+ floorId?: string;
106
+ };
107
+
94
108
  export type AgentGameOfficeCameraState = {
95
109
  zoom: number;
96
110
  distance: number;
@@ -114,8 +128,10 @@ export type AgentGameOfficeMountOptions = {
114
128
  source: AgentGameOfficeSourceInput;
115
129
  office?: AgentGameOfficeConfig;
116
130
  focusedAgentId?: string | null;
131
+ focusedFloorId?: string | null;
117
132
  className?: string;
118
133
  onCameraChange?: (state: AgentGameOfficeCameraState) => void;
134
+ onFocusedFloorChange?: (floorId: string | null) => void;
119
135
  };
120
136
 
121
137
  export type AgentGameOfficeResolvedOptions = Omit<AgentGameOfficeMountOptions, "renderer"> & {
@@ -125,7 +141,11 @@ export type AgentGameOfficeResolvedOptions = Omit<AgentGameOfficeMountOptions, "
125
141
 
126
142
  export type AgentGameOfficeController = {
127
143
  updateAgents(agents: AgentPresence[]): void;
144
+ resetCamera(): void;
128
145
  focusAgent(agentId: string | null): void;
146
+ focusFloor(floorId: string | null): void;
147
+ getFocusedFloor(): string | null;
129
148
  refreshAgents(): void;
149
+ getNavigationErrors(): AgentNavigationError[];
130
150
  destroy(): void;
131
151
  };
@@ -1,75 +1,284 @@
1
- export type AgentGameOfficeRoomType = "office" | "auditorium" | "gym";
1
+ export type AgentGameOfficeRoomType = "office" | "auditorium" | "gym" | "dining" | "cafe" | "skyGarden";
2
2
 
3
3
  export type AgentGameOfficeRoomConfig = {
4
4
  type: AgentGameOfficeRoomType;
5
5
  capacity?: number;
6
6
  };
7
7
 
8
- export type AgentGameOfficeConfig = {
8
+ export type AgentGameOfficeConnectorType = "elevator";
9
+
10
+ export type AgentGameOfficeFloorConfig = {
11
+ id: string;
12
+ name: string;
9
13
  rooms: AgentGameOfficeRoomConfig[];
10
14
  };
11
15
 
16
+ export type AgentGameOfficeConnectorConfig = {
17
+ id: string;
18
+ type: AgentGameOfficeConnectorType;
19
+ name: string;
20
+ serves: string[];
21
+ };
22
+
23
+ export type AgentGameOfficeConfig = {
24
+ building: {
25
+ floors: AgentGameOfficeFloorConfig[];
26
+ connectors: AgentGameOfficeConnectorConfig[];
27
+ };
28
+ };
29
+
12
30
  export type NormalizedOfficeRoomConfig = {
13
31
  id: string;
14
32
  type: AgentGameOfficeRoomType;
15
33
  capacity: number;
16
34
  };
17
35
 
18
- export type NormalizedOfficeConfig = {
36
+ export type NormalizedOfficeFloorConfig = {
37
+ id: string;
38
+ name: string;
39
+ level: number;
19
40
  rooms: NormalizedOfficeRoomConfig[];
20
41
  };
21
42
 
43
+ export type NormalizedOfficeConnectorConfig = {
44
+ id: string;
45
+ type: AgentGameOfficeConnectorType;
46
+ name: string;
47
+ serves: string[];
48
+ };
49
+
50
+ export type NormalizedOfficeConfig = {
51
+ building: {
52
+ floors: NormalizedOfficeFloorConfig[];
53
+ connectors: NormalizedOfficeConnectorConfig[];
54
+ };
55
+ };
56
+
22
57
  const DEFAULT_ROOM_CAPACITY = {
23
58
  office: 12,
24
59
  auditorium: 40,
25
60
  gym: 8,
61
+ dining: 16,
62
+ cafe: 12,
63
+ skyGarden: 12,
26
64
  } satisfies Record<AgentGameOfficeRoomType, number>;
27
65
 
28
66
  const MAX_ROOM_CAPACITY = {
29
67
  office: 48,
30
68
  auditorium: 120,
31
69
  gym: 24,
70
+ dining: 48,
71
+ cafe: 32,
72
+ skyGarden: 32,
32
73
  } satisfies Record<AgentGameOfficeRoomType, number>;
33
74
 
34
75
  export const DEFAULT_OFFICE_CONFIG: AgentGameOfficeConfig = {
35
- rooms: [
36
- { type: "office" },
37
- { type: "auditorium" },
38
- { type: "gym" },
39
- ],
76
+ building: {
77
+ floors: [
78
+ {
79
+ id: "floor-1",
80
+ name: "1F",
81
+ rooms: [
82
+ { type: "office" },
83
+ { type: "auditorium" },
84
+ { type: "gym" },
85
+ ],
86
+ },
87
+ ],
88
+ connectors: [],
89
+ },
40
90
  };
41
91
 
42
92
  export function normalizeOfficeConfig(
43
93
  config: AgentGameOfficeConfig | undefined = DEFAULT_OFFICE_CONFIG,
44
94
  ): NormalizedOfficeConfig {
45
- if (!Array.isArray(config.rooms) || config.rooms.length === 0) {
46
- throw new Error("Agent game office config requires at least one room");
95
+ if (isRecord(config) && "rooms" in config) {
96
+ throw new Error("Agent game office config must use building floors instead of top-level rooms");
97
+ }
98
+ if (!config || !isRecord(config.building)) {
99
+ throw new Error("Agent game office config requires a building");
100
+ }
101
+
102
+ const floors = config.building.floors;
103
+ if (!Array.isArray(floors) || floors.length === 0) {
104
+ throw new Error("Agent game office building requires at least one floor");
105
+ }
106
+
107
+ const floorIds = new Set<string>();
108
+ const normalizedFloors = floors.map((floor, level) => {
109
+ if (!isRecord(floor)) {
110
+ throw new Error("Agent game office building floor config must be an object");
111
+ }
112
+ const floorId = assertNonEmptyId(floor.id, "floor");
113
+ assertUnique(floorId, floorIds, "floor");
114
+ if (!Array.isArray(floor.rooms) || floor.rooms.length === 0) {
115
+ throw new Error(`Agent game office floor ${floorId} requires at least one room`);
116
+ }
117
+
118
+ const counts = new Map<AgentGameOfficeRoomType, number>();
119
+ return {
120
+ id: floorId,
121
+ name: normalizeName(floor.name, "floor", floorId),
122
+ level,
123
+ rooms: floor.rooms.map((room) => {
124
+ if (!isRecord(room)) {
125
+ throw new Error(`Agent game office floor ${floorId} room config must be an object`);
126
+ }
127
+ assertRoomType(room.type);
128
+ const nextCount = (counts.get(room.type) ?? 0) + 1;
129
+ counts.set(room.type, nextCount);
130
+ return {
131
+ id: `${floorId}:${room.type}-${nextCount}`,
132
+ type: room.type,
133
+ capacity: normalizeRoomCapacity(room.type, room.capacity),
134
+ };
135
+ }),
136
+ };
137
+ });
138
+
139
+ const connectors = config.building.connectors;
140
+ if (!Array.isArray(connectors)) {
141
+ throw new Error("Agent game office building connectors must be an array");
142
+ }
143
+ if (normalizedFloors.length === 1 && connectors.length > 0) {
144
+ throw new Error("Agent game office single-floor building should not define connectors");
47
145
  }
146
+ if (normalizedFloors.length > 1 && connectors.length === 0) {
147
+ throw new Error("Agent game office building with multiple floors requires at least one connector");
148
+ }
149
+
150
+ const connectorIds = new Set<string>();
151
+ const normalizedConnectors = connectors.map((connector) => {
152
+ if (!isRecord(connector)) {
153
+ throw new Error("Agent game office building connector config must be an object");
154
+ }
155
+ const connectorId = assertNonEmptyId(connector.id, "connector");
156
+ assertUnique(connectorId, connectorIds, "connector");
157
+ assertConnectorType(connector.type);
158
+ if (!Array.isArray(connector.serves)) {
159
+ throw new Error(`Agent game office connector ${connectorId} must serve at least two floors`);
160
+ }
161
+ const servedFloorIds = new Set<string>();
162
+ const serves = connector.serves.map((floorId) => {
163
+ const normalizedFloorId = assertNonEmptyId(floorId, "connector floor");
164
+ if (!floorIds.has(normalizedFloorId)) {
165
+ throw new Error(`Agent game office connector ${connectorId} serves unknown floor: ${normalizedFloorId}`);
166
+ }
167
+ assertUnique(normalizedFloorId, servedFloorIds, "connector served floor");
168
+ return normalizedFloorId;
169
+ });
170
+ if (serves.length < 2) {
171
+ throw new Error(`Agent game office connector ${connectorId} must serve at least two floors`);
172
+ }
173
+
174
+ return {
175
+ id: connectorId,
176
+ type: connector.type,
177
+ name: normalizeName(connector.name, "connector", connectorId),
178
+ serves,
179
+ };
180
+ });
181
+ validateConnectorCoverage(normalizedFloors.map((floor) => floor.id), normalizedConnectors);
48
182
 
49
- const counts = new Map<AgentGameOfficeRoomType, number>();
50
183
  return {
51
- rooms: config.rooms.map((room) => {
52
- assertRoomType(room.type);
53
- const nextCount = (counts.get(room.type) ?? 0) + 1;
54
- counts.set(room.type, nextCount);
55
- return {
56
- id: `${room.type}-${nextCount}`,
57
- type: room.type,
58
- capacity: normalizeRoomCapacity(room.type, room.capacity),
59
- };
60
- }),
184
+ building: {
185
+ floors: normalizedFloors,
186
+ connectors: normalizedConnectors,
187
+ },
61
188
  };
62
189
  }
63
190
 
64
- function assertRoomType(value: string): asserts value is AgentGameOfficeRoomType {
65
- if (value !== "office" && value !== "auditorium" && value !== "gym") {
191
+ function validateConnectorCoverage(
192
+ floorIds: string[],
193
+ connectors: NormalizedOfficeConnectorConfig[],
194
+ ) {
195
+ if (floorIds.length <= 1) {
196
+ return;
197
+ }
198
+
199
+ const neighbors = new Map(floorIds.map((floorId) => [floorId, new Set<string>()]));
200
+ for (const connector of connectors) {
201
+ for (let leftIndex = 0; leftIndex < connector.serves.length; leftIndex += 1) {
202
+ for (let rightIndex = leftIndex + 1; rightIndex < connector.serves.length; rightIndex += 1) {
203
+ const left = connector.serves[leftIndex]!;
204
+ const right = connector.serves[rightIndex]!;
205
+ neighbors.get(left)?.add(right);
206
+ neighbors.get(right)?.add(left);
207
+ }
208
+ }
209
+ }
210
+
211
+ const visited = new Set<string>();
212
+ const queue = [floorIds[0]!];
213
+ while (queue.length > 0) {
214
+ const floorId = queue.shift()!;
215
+ if (visited.has(floorId)) {
216
+ continue;
217
+ }
218
+ visited.add(floorId);
219
+ for (const next of neighbors.get(floorId) ?? []) {
220
+ if (!visited.has(next)) {
221
+ queue.push(next);
222
+ }
223
+ }
224
+ }
225
+
226
+ if (visited.size !== floorIds.length) {
227
+ throw new Error("Agent game office building connector graph is disconnected");
228
+ }
229
+ }
230
+
231
+ function isRecord(value: unknown): value is Record<string, unknown> {
232
+ return typeof value === "object" && value !== null;
233
+ }
234
+
235
+ function assertNonEmptyId(value: unknown, label: string): string {
236
+ if (typeof value !== "string") {
237
+ throw new Error(`Agent game office ${label} id must be non-empty`);
238
+ }
239
+ const normalized = value.trim();
240
+ if (!normalized) {
241
+ throw new Error(`Agent game office ${label} id must be non-empty`);
242
+ }
243
+ return normalized;
244
+ }
245
+
246
+ function assertUnique(value: string, seen: Set<string>, label: string) {
247
+ if (seen.has(value)) {
248
+ throw new Error(`duplicate agent game office ${label} id: ${value}`);
249
+ }
250
+ seen.add(value);
251
+ }
252
+
253
+ function normalizeName(value: unknown, label: string, id: string): string {
254
+ if (typeof value !== "string" || !value.trim()) {
255
+ throw new Error(`Agent game office ${label} ${id} name must be non-empty`);
256
+ }
257
+ return value.trim();
258
+ }
259
+
260
+ function assertRoomType(value: unknown): asserts value is AgentGameOfficeRoomType {
261
+ if (
262
+ value !== "office"
263
+ && value !== "auditorium"
264
+ && value !== "gym"
265
+ && value !== "dining"
266
+ && value !== "cafe"
267
+ && value !== "skyGarden"
268
+ ) {
66
269
  throw new Error(`Unsupported agent game office room type: ${value}`);
67
270
  }
68
271
  }
69
272
 
70
- function normalizeRoomCapacity(type: AgentGameOfficeRoomType, capacity: number | undefined): number {
273
+ function assertConnectorType(value: unknown): asserts value is AgentGameOfficeConnectorType {
274
+ if (value !== "elevator") {
275
+ throw new Error(`Unsupported agent game office connector type: ${value}`);
276
+ }
277
+ }
278
+
279
+ function normalizeRoomCapacity(type: AgentGameOfficeRoomType, capacity: unknown): number {
71
280
  const value = capacity ?? DEFAULT_ROOM_CAPACITY[type];
72
- if (!Number.isInteger(value) || value <= 0) {
281
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
73
282
  throw new Error(`Agent game office room ${type} capacity must be a positive integer`);
74
283
  }
75
284
  return Math.min(value, MAX_ROOM_CAPACITY[type]);
@@ -1,2 +1,3 @@
1
1
  export * from "./config";
2
+ export * from "./navigation";
2
3
  export * from "./resolver";
@@ -0,0 +1,168 @@
1
+ export type Vector3Like = { x: number; y: number; z: number };
2
+
3
+ export type NavigationNodeKind =
4
+ | "seat"
5
+ | "zone"
6
+ | "connector-entry"
7
+ | "connector-cabin"
8
+ | "offstage";
9
+
10
+ export type NavigationNode = {
11
+ id: string;
12
+ floorId: string | null;
13
+ kind: NavigationNodeKind;
14
+ anchorId?: string;
15
+ connectorId?: string;
16
+ position: Vector3Like;
17
+ };
18
+
19
+ export type NavigationEdge =
20
+ | { kind: "walk"; from: string; to: string; floorId: string }
21
+ | {
22
+ kind: "elevator";
23
+ from: string;
24
+ to: string;
25
+ connectorId: string;
26
+ fromFloorId: string;
27
+ toFloorId: string;
28
+ };
29
+
30
+ export type ResolvedBuildingNavigationGraph = {
31
+ nodes: NavigationNode[];
32
+ edges: NavigationEdge[];
33
+ };
34
+
35
+ export type AgentRouteStep =
36
+ | {
37
+ kind: "walk";
38
+ fromNodeId: string;
39
+ toNodeId: string;
40
+ floorId: string;
41
+ from: Vector3Like;
42
+ to: Vector3Like;
43
+ }
44
+ | {
45
+ kind: "elevator";
46
+ connectorId: string;
47
+ fromFloorId: string;
48
+ toFloorId: string;
49
+ fromNodeId: string;
50
+ toNodeId: string;
51
+ from: Vector3Like;
52
+ to: Vector3Like;
53
+ };
54
+
55
+ export type AgentRoute = {
56
+ id: string;
57
+ agentId?: string;
58
+ fromNodeId: string;
59
+ toNodeId: string;
60
+ steps: AgentRouteStep[];
61
+ };
62
+
63
+ export class NoBuildingRouteError extends Error {
64
+ constructor(fromNodeId: string, toNodeId: string) {
65
+ super(`No building navigation route from ${fromNodeId} to ${toNodeId}`);
66
+ this.name = "NoBuildingRouteError";
67
+ }
68
+ }
69
+
70
+ export function planAgentRoute(
71
+ graph: ResolvedBuildingNavigationGraph,
72
+ fromNodeId: string,
73
+ toNodeId: string,
74
+ ): AgentRoute {
75
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
76
+ if (!nodes.has(fromNodeId) || !nodes.has(toNodeId)) {
77
+ throw new NoBuildingRouteError(fromNodeId, toNodeId);
78
+ }
79
+ if (fromNodeId === toNodeId) {
80
+ return {
81
+ id: `${fromNodeId}->${toNodeId}`,
82
+ fromNodeId,
83
+ toNodeId,
84
+ steps: [],
85
+ };
86
+ }
87
+
88
+ const edgesByFrom = new Map<string, NavigationEdge[]>();
89
+ for (const edge of graph.edges) {
90
+ const edges = edgesByFrom.get(edge.from);
91
+ if (edges) {
92
+ edges.push(edge);
93
+ } else {
94
+ edgesByFrom.set(edge.from, [edge]);
95
+ }
96
+ }
97
+
98
+ const queue = [fromNodeId];
99
+ const visited = new Set<string>([fromNodeId]);
100
+ const previous = new Map<string, NavigationEdge>();
101
+
102
+ while (queue.length > 0) {
103
+ const current = queue.shift()!;
104
+ for (const edge of edgesByFrom.get(current) ?? []) {
105
+ if (visited.has(edge.to)) {
106
+ continue;
107
+ }
108
+ visited.add(edge.to);
109
+ previous.set(edge.to, edge);
110
+ if (edge.to === toNodeId) {
111
+ queue.length = 0;
112
+ break;
113
+ }
114
+ queue.push(edge.to);
115
+ }
116
+ }
117
+
118
+ if (!previous.has(toNodeId)) {
119
+ throw new NoBuildingRouteError(fromNodeId, toNodeId);
120
+ }
121
+
122
+ const path: NavigationEdge[] = [];
123
+ let cursor = toNodeId;
124
+ while (cursor !== fromNodeId) {
125
+ const edge = previous.get(cursor);
126
+ if (!edge) {
127
+ throw new NoBuildingRouteError(fromNodeId, toNodeId);
128
+ }
129
+ path.push(edge);
130
+ cursor = edge.from;
131
+ }
132
+ path.reverse();
133
+
134
+ return {
135
+ id: `${fromNodeId}->${toNodeId}`,
136
+ fromNodeId,
137
+ toNodeId,
138
+ steps: path.map((edge) => edgeToRouteStep(edge, nodes)),
139
+ };
140
+ }
141
+
142
+ function edgeToRouteStep(edge: NavigationEdge, nodes: Map<string, NavigationNode>): AgentRouteStep {
143
+ const from = nodes.get(edge.from);
144
+ const to = nodes.get(edge.to);
145
+ if (!from || !to) {
146
+ throw new NoBuildingRouteError(edge.from, edge.to);
147
+ }
148
+ if (edge.kind === "walk") {
149
+ return {
150
+ kind: "walk",
151
+ fromNodeId: edge.from,
152
+ toNodeId: edge.to,
153
+ floorId: edge.floorId,
154
+ from: from.position,
155
+ to: to.position,
156
+ };
157
+ }
158
+ return {
159
+ kind: "elevator",
160
+ connectorId: edge.connectorId,
161
+ fromFloorId: edge.fromFloorId,
162
+ toFloorId: edge.toFloorId,
163
+ fromNodeId: edge.from,
164
+ toNodeId: edge.to,
165
+ from: from.position,
166
+ to: to.position,
167
+ };
168
+ }