@agent-os-lab/agent-game-sdk 0.1.1

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.
Files changed (68) hide show
  1. package/README.md +99 -0
  2. package/package.json +38 -0
  3. package/src/core/agent-game-store.ts +110 -0
  4. package/src/core/agent-service-event-adapter.ts +20 -0
  5. package/src/core/assets.ts +119 -0
  6. package/src/core/commands.ts +42 -0
  7. package/src/core/errors.ts +19 -0
  8. package/src/core/event-adapter.ts +40 -0
  9. package/src/core/index.ts +23 -0
  10. package/src/core/life-presets.ts +54 -0
  11. package/src/core/movement.ts +50 -0
  12. package/src/core/office-building-layout.ts +376 -0
  13. package/src/core/office-layout.ts +152 -0
  14. package/src/core/pixel-character-avatar.ts +87 -0
  15. package/src/core/pixel-character.ts +684 -0
  16. package/src/core/realtime-events.ts +44 -0
  17. package/src/core/realtime-transport.ts +39 -0
  18. package/src/core/reducer.ts +105 -0
  19. package/src/core/scene.ts +144 -0
  20. package/src/core/schedule.ts +20 -0
  21. package/src/core/sequence.ts +48 -0
  22. package/src/core/state.ts +26 -0
  23. package/src/core/svg-pixel-avatar.ts +372 -0
  24. package/src/core/town-office-assets.ts +109 -0
  25. package/src/core/town-office-room-presets.ts +455 -0
  26. package/src/core/town-office-seat-layout.ts +238 -0
  27. package/src/graph.ts +112 -0
  28. package/src/index.ts +2 -0
  29. package/src/office/core/projection.ts +89 -0
  30. package/src/office/core/source.ts +46 -0
  31. package/src/office/core/types.ts +110 -0
  32. package/src/office/index.ts +4 -0
  33. package/src/office/mount.ts +104 -0
  34. package/src/office/react/AgentGameOfficeView.ts +58 -0
  35. package/src/office/react/index.ts +1 -0
  36. package/src/office/renderers/three/agent-activity-effects.ts +161 -0
  37. package/src/office/renderers/three/agent-animation.ts +205 -0
  38. package/src/office/renderers/three/agent-body-instancing.ts +119 -0
  39. package/src/office/renderers/three/agent-label.ts +82 -0
  40. package/src/office/renderers/three/agent-layout.ts +72 -0
  41. package/src/office/renderers/three/agent-mesh.ts +145 -0
  42. package/src/office/renderers/three/mount.ts +253 -0
  43. package/src/office/renderers/three/scene.ts +790 -0
  44. package/src/phaser/agent-game-scene.ts +87 -0
  45. package/src/phaser/anchor-debug.ts +22 -0
  46. package/src/phaser/avatar-registry.ts +46 -0
  47. package/src/phaser/camera-controls.ts +419 -0
  48. package/src/phaser/camera-model.ts +81 -0
  49. package/src/phaser/create-agent-game.ts +242 -0
  50. package/src/phaser/debug-overlay.ts +21 -0
  51. package/src/phaser/index.ts +13 -0
  52. package/src/phaser/movement-tween.ts +59 -0
  53. package/src/phaser/office-background.ts +48 -0
  54. package/src/phaser/office-building-renderer.ts +87 -0
  55. package/src/phaser/office-layout-renderer.ts +58 -0
  56. package/src/phaser/render-layers.ts +30 -0
  57. package/src/phaser/scene-reconciler.ts +614 -0
  58. package/src/phaser/scene-renderer.ts +138 -0
  59. package/src/phaser/text-style.ts +8 -0
  60. package/src/phaser/town-office-business-props.ts +256 -0
  61. package/src/phaser/town-office-environment.ts +89 -0
  62. package/src/phaser/town-office-furniture.ts +182 -0
  63. package/src/phaser/town-office-primitives.ts +53 -0
  64. package/src/phaser/town-office-renderer.ts +429 -0
  65. package/src/phaser/types.ts +67 -0
  66. package/src/phaser/viewport.ts +88 -0
  67. package/src/runtime-client.ts +435 -0
  68. package/src/types.ts +80 -0
@@ -0,0 +1,435 @@
1
+ export type AgentPresenceStatus =
2
+ | "working"
3
+ | "thinking"
4
+ | "meeting"
5
+ | "resting"
6
+ | "entertaining"
7
+ | "idle"
8
+ | "offline";
9
+
10
+ export type AgentPresenceStatusSource = "runtime" | "simulation" | "system";
11
+
12
+ export type AgentActivityDisplay = {
13
+ kind: "received" | "replying" | "replied" | "tool" | "running" | "completed";
14
+ summary: string;
15
+ preview?: string;
16
+ updatedAt: string;
17
+ expiresAt?: string;
18
+ };
19
+
20
+ export type AgentPresence = {
21
+ tenantId: string;
22
+ agentId: string;
23
+ displayName: string;
24
+ status: AgentPresenceStatus;
25
+ statusSource: AgentPresenceStatusSource;
26
+ activeRunId?: string;
27
+ scene?: string;
28
+ mood?: string;
29
+ activity?: AgentActivityDisplay;
30
+ updatedAt: string;
31
+ expiresAt?: string;
32
+ };
33
+
34
+ export type GameRuntimeBootstrapStatus = "active" | "pending" | "failed";
35
+
36
+ export type GameRuntimeSnapshotMessage = {
37
+ type: "snapshot";
38
+ tenantId: string;
39
+ bootstrapStatus: GameRuntimeBootstrapStatus;
40
+ agents: AgentPresence[];
41
+ sequence: number;
42
+ serverTime: string;
43
+ };
44
+
45
+ export type GameRuntimePatchMessage = {
46
+ type: "patch";
47
+ tenantId: string;
48
+ agents: AgentPresence[];
49
+ sequence: number;
50
+ serverTime: string;
51
+ };
52
+
53
+ export type GameRuntimeServerMessage =
54
+ | GameRuntimeSnapshotMessage
55
+ | GameRuntimePatchMessage;
56
+
57
+ export type GameRuntimeConnectionState = "connecting" | "online" | "closed";
58
+
59
+ export type GameRuntimeSubscription = {
60
+ close: () => void;
61
+ getSocket: () => WebSocket | null;
62
+ };
63
+
64
+ export type AgentGameRuntimeBrowserClientOptions = {
65
+ baseUrl: string;
66
+ tokenPath?: string;
67
+ fetch?: typeof fetch;
68
+ accessToken?: string | (() => string | undefined | Promise<string | undefined>);
69
+ requestId?: string | (() => string | undefined);
70
+ headers?: HeadersInit | (() => HeadersInit | undefined);
71
+ WebSocketImpl?: typeof WebSocket;
72
+ };
73
+
74
+ export type AgentGameRuntimeServerClientOptions = {
75
+ baseUrl: string;
76
+ apiKey: string;
77
+ tokenPath?: string;
78
+ fetch?: typeof fetch;
79
+ requestId?: string | (() => string | undefined);
80
+ headers?: HeadersInit | (() => HeadersInit | undefined);
81
+ };
82
+
83
+ export type AgentGameRuntimeBrowserRequestOptions = {
84
+ requestId?: string;
85
+ signal?: AbortSignal;
86
+ headers?: HeadersInit;
87
+ };
88
+
89
+ export type AgentGameRuntimeServerRequestOptions = {
90
+ requestId?: string;
91
+ signal?: AbortSignal;
92
+ headers?: HeadersInit;
93
+ };
94
+
95
+ export type GameRuntimeTokenResponse = {
96
+ tenantId: string;
97
+ publicWsUrl: string;
98
+ token: string;
99
+ expiresAt: string;
100
+ };
101
+
102
+ export type SubscribeGameRuntimeOptions = {
103
+ tenantId: string;
104
+ publicWsUrl: string;
105
+ token: string;
106
+ pingIntervalMs?: number;
107
+ WebSocketImpl?: typeof WebSocket;
108
+ onOpen?: () => void;
109
+ onClose?: () => void;
110
+ onError?: (error: Event | Error) => void;
111
+ onMessage?: (message: GameRuntimeServerMessage) => void;
112
+ onSnapshot?: (message: GameRuntimeSnapshotMessage) => void;
113
+ onPatch?: (message: GameRuntimePatchMessage) => void;
114
+ };
115
+
116
+ export type AgentGameRuntimeBrowserSubscribeOptions =
117
+ AgentGameRuntimeBrowserRequestOptions &
118
+ Omit<SubscribeGameRuntimeOptions, "tenantId" | "publicWsUrl" | "token" | "WebSocketImpl">;
119
+
120
+ export class AgentGameRuntimeBrowserClient {
121
+ private readonly baseUrl: string;
122
+ private readonly tokenPath: string;
123
+ private readonly fetchImpl: typeof fetch;
124
+ private readonly accessToken?: AgentGameRuntimeBrowserClientOptions["accessToken"];
125
+ private readonly requestId?: AgentGameRuntimeBrowserClientOptions["requestId"];
126
+ private readonly customHeaders?: AgentGameRuntimeBrowserClientOptions["headers"];
127
+ private readonly WebSocketImpl?: typeof WebSocket;
128
+
129
+ constructor(options: AgentGameRuntimeBrowserClientOptions) {
130
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
131
+ this.tokenPath = options.tokenPath ?? "/api/agent-game-runtime-token";
132
+ this.fetchImpl = options.fetch ?? boundFetch();
133
+ this.accessToken = options.accessToken;
134
+ this.requestId = options.requestId;
135
+ this.customHeaders = options.headers;
136
+ this.WebSocketImpl = options.WebSocketImpl;
137
+ }
138
+
139
+ async subscribe(
140
+ options: AgentGameRuntimeBrowserSubscribeOptions = {},
141
+ ): Promise<GameRuntimeSubscription> {
142
+ const tokenResponse = await this.fetchToken(options);
143
+ return subscribeGameRuntime({
144
+ ...options,
145
+ tenantId: tokenResponse.tenantId,
146
+ publicWsUrl: tokenResponse.publicWsUrl,
147
+ token: tokenResponse.token,
148
+ WebSocketImpl: this.WebSocketImpl,
149
+ });
150
+ }
151
+
152
+ private async fetchToken(
153
+ options: AgentGameRuntimeBrowserRequestOptions,
154
+ ): Promise<GameRuntimeTokenResponse> {
155
+ const response = await this.fetchImpl(this.url(this.tokenPath), {
156
+ method: "GET",
157
+ headers: await this.headers(options),
158
+ signal: options.signal,
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`Game runtime token request failed with status ${response.status}.`);
162
+ }
163
+ const body = await response.json() as GameRuntimeTokenResponse;
164
+ if (!isGameRuntimeTokenResponse(body)) {
165
+ throw new Error("Game runtime token response is invalid.");
166
+ }
167
+ return body;
168
+ }
169
+
170
+ private async headers(
171
+ requestOptions?: AgentGameRuntimeBrowserRequestOptions,
172
+ ): Promise<Headers> {
173
+ const headers = safeBrowserHeaders(resolveHeaders(this.customHeaders));
174
+ mergeSafeBrowserHeaders(headers, requestOptions?.headers);
175
+ const token = await resolveAccessToken(this.accessToken);
176
+ if (token) {
177
+ headers.set("authorization", `Bearer ${token}`);
178
+ }
179
+ const requestId = requestOptions?.requestId ?? resolveRequestId(this.requestId);
180
+ if (requestId) {
181
+ headers.set("x-hermes-request-id", requestId);
182
+ }
183
+ return headers;
184
+ }
185
+
186
+ private url(path: string): string {
187
+ if (/^https?:\/\//.test(path)) {
188
+ return path;
189
+ }
190
+ return `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
191
+ }
192
+ }
193
+
194
+ export class AgentGameRuntimeServerClient {
195
+ private readonly baseUrl: string;
196
+ private readonly apiKey: string;
197
+ private readonly tokenPath: string;
198
+ private readonly fetchImpl: typeof fetch;
199
+ private readonly requestId?: AgentGameRuntimeServerClientOptions["requestId"];
200
+ private readonly customHeaders?: AgentGameRuntimeServerClientOptions["headers"];
201
+
202
+ constructor(options: AgentGameRuntimeServerClientOptions) {
203
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
204
+ this.apiKey = options.apiKey;
205
+ this.tokenPath = options.tokenPath ?? "/api/v1/game-runtime-token";
206
+ this.fetchImpl = options.fetch ?? boundFetch();
207
+ this.requestId = options.requestId;
208
+ this.customHeaders = options.headers;
209
+ }
210
+
211
+ async createRuntimeToken(
212
+ options: AgentGameRuntimeServerRequestOptions = {},
213
+ ): Promise<GameRuntimeTokenResponse> {
214
+ const response = await this.fetchImpl(this.url(this.tokenPath), {
215
+ method: "GET",
216
+ headers: this.headers(options),
217
+ signal: options.signal,
218
+ });
219
+ if (!response.ok) {
220
+ throw new Error(`Game runtime token request failed with status ${response.status}.`);
221
+ }
222
+ const body = await response.json() as GameRuntimeTokenResponse;
223
+ if (!isGameRuntimeTokenResponse(body)) {
224
+ throw new Error("Game runtime token response is invalid.");
225
+ }
226
+ return body;
227
+ }
228
+
229
+ private headers(requestOptions?: AgentGameRuntimeServerRequestOptions): Headers {
230
+ const headers = new Headers(resolveHeaders(this.customHeaders));
231
+ mergeHeaders(headers, requestOptions?.headers);
232
+ headers.set("authorization", `Bearer ${this.apiKey}`);
233
+ const requestId = requestOptions?.requestId ?? resolveRequestId(this.requestId);
234
+ if (requestId) {
235
+ headers.set("x-hermes-request-id", requestId);
236
+ }
237
+ return headers;
238
+ }
239
+
240
+ private url(path: string): string {
241
+ if (/^https?:\/\//.test(path)) {
242
+ return path;
243
+ }
244
+ return `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
245
+ }
246
+ }
247
+
248
+ export function subscribeGameRuntime(
249
+ options: SubscribeGameRuntimeOptions,
250
+ ): GameRuntimeSubscription {
251
+ const WebSocketCtor = options.WebSocketImpl ?? globalThis.WebSocket;
252
+ if (!WebSocketCtor) {
253
+ throw new Error("WebSocket is not available in this runtime.");
254
+ }
255
+
256
+ let closed = false;
257
+ let pingInterval: ReturnType<typeof setInterval> | null = null;
258
+ const socket = new WebSocketCtor(createGameRuntimeWebSocketUrl(
259
+ options.publicWsUrl,
260
+ options.token,
261
+ ));
262
+
263
+ socket.addEventListener("open", () => {
264
+ if (closed) {
265
+ return;
266
+ }
267
+ options.onOpen?.();
268
+ socket.send(JSON.stringify({
269
+ type: "subscribe",
270
+ tenantId: options.tenantId,
271
+ }));
272
+ pingInterval = setInterval(() => {
273
+ if (socket.readyState === WebSocketCtor.OPEN) {
274
+ socket.send(JSON.stringify({ type: "ping" }));
275
+ }
276
+ }, options.pingIntervalMs ?? 25_000);
277
+ });
278
+
279
+ socket.addEventListener("message", (event) => {
280
+ const message = parseGameRuntimeMessage(event.data);
281
+ if (!message || closed) {
282
+ return;
283
+ }
284
+ options.onMessage?.(message);
285
+ if (message.type === "snapshot") {
286
+ options.onSnapshot?.(message);
287
+ } else {
288
+ options.onPatch?.(message);
289
+ }
290
+ });
291
+
292
+ socket.addEventListener("close", () => {
293
+ clearPingInterval();
294
+ if (!closed) {
295
+ options.onClose?.();
296
+ }
297
+ });
298
+
299
+ socket.addEventListener("error", (event) => {
300
+ if (!closed) {
301
+ options.onError?.(event);
302
+ }
303
+ });
304
+
305
+ function clearPingInterval() {
306
+ if (pingInterval) {
307
+ clearInterval(pingInterval);
308
+ pingInterval = null;
309
+ }
310
+ }
311
+
312
+ return {
313
+ close: () => {
314
+ closed = true;
315
+ clearPingInterval();
316
+ socket.close();
317
+ },
318
+ getSocket: () => socket,
319
+ };
320
+ }
321
+
322
+ export function createGameRuntimeWebSocketUrl(
323
+ publicWsUrl: string,
324
+ token: string,
325
+ ): string {
326
+ const url = new URL(publicWsUrl);
327
+ url.searchParams.set("token", token);
328
+ return url.toString();
329
+ }
330
+
331
+ export function parseGameRuntimeMessage(
332
+ value: unknown,
333
+ ): GameRuntimeServerMessage | null {
334
+ try {
335
+ const parsed = JSON.parse(String(value)) as { type?: string };
336
+ return parsed.type === "snapshot" || parsed.type === "patch"
337
+ ? parsed as GameRuntimeServerMessage
338
+ : null;
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+
344
+ export function mergeAgentPresence(
345
+ current: AgentPresence[],
346
+ changed: AgentPresence[],
347
+ ): AgentPresence[] {
348
+ const next = new Map(current.map((agent) => [agent.agentId, agent]));
349
+ for (const agent of changed) {
350
+ next.set(agent.agentId, agent);
351
+ }
352
+ return Array.from(next.values()).sort((left, right) =>
353
+ left.displayName.localeCompare(right.displayName),
354
+ );
355
+ }
356
+
357
+ export function countAgentPresenceStatuses(
358
+ agents: AgentPresence[],
359
+ ): Record<AgentPresenceStatus, number> {
360
+ const counts = {
361
+ working: 0,
362
+ thinking: 0,
363
+ meeting: 0,
364
+ resting: 0,
365
+ entertaining: 0,
366
+ idle: 0,
367
+ offline: 0,
368
+ } satisfies Record<AgentPresenceStatus, number>;
369
+
370
+ for (const agent of agents) {
371
+ counts[agent.status] += 1;
372
+ }
373
+ return counts;
374
+ }
375
+
376
+ function boundFetch(): typeof fetch {
377
+ return globalThis.fetch.bind(globalThis);
378
+ }
379
+
380
+ function resolveHeaders(
381
+ value: AgentGameRuntimeBrowserClientOptions["headers"] | AgentGameRuntimeServerClientOptions["headers"],
382
+ ): HeadersInit | undefined {
383
+ return typeof value === "function" ? value() : value;
384
+ }
385
+
386
+ async function resolveAccessToken(
387
+ value: AgentGameRuntimeBrowserClientOptions["accessToken"],
388
+ ): Promise<string | undefined> {
389
+ return typeof value === "function" ? await value() : value;
390
+ }
391
+
392
+ function resolveRequestId(
393
+ value: AgentGameRuntimeBrowserClientOptions["requestId"],
394
+ ): string | undefined {
395
+ return typeof value === "function" ? value() : value;
396
+ }
397
+
398
+ function safeBrowserHeaders(source?: HeadersInit): Headers {
399
+ const headers = new Headers(source);
400
+ stripForbiddenBrowserHeaders(headers);
401
+ return headers;
402
+ }
403
+
404
+ function mergeSafeBrowserHeaders(target: Headers, source?: HeadersInit): void {
405
+ const headers = safeBrowserHeaders(source);
406
+ for (const [key, value] of headers.entries()) {
407
+ target.set(key, value);
408
+ }
409
+ }
410
+
411
+ function mergeHeaders(target: Headers, source?: HeadersInit): void {
412
+ const headers = new Headers(source);
413
+ for (const [key, value] of headers.entries()) {
414
+ target.set(key, value);
415
+ }
416
+ }
417
+
418
+ function stripForbiddenBrowserHeaders(headers: Headers): void {
419
+ headers.delete("authorization");
420
+ headers.delete("x-hermes-tenant-id");
421
+ headers.delete("x-agentos-tenant-id");
422
+ }
423
+
424
+ function isGameRuntimeTokenResponse(value: unknown): value is GameRuntimeTokenResponse {
425
+ if (!value || typeof value !== "object") {
426
+ return false;
427
+ }
428
+ const response = value as Partial<GameRuntimeTokenResponse>;
429
+ return (
430
+ typeof response.tenantId === "string" &&
431
+ typeof response.publicWsUrl === "string" &&
432
+ typeof response.token === "string" &&
433
+ typeof response.expiresAt === "string"
434
+ );
435
+ }
package/src/types.ts ADDED
@@ -0,0 +1,80 @@
1
+ export type AgentGameNodeKind =
2
+ | "agent"
3
+ | "profile"
4
+ | "session"
5
+ | "memory"
6
+ | "tool"
7
+ | "message"
8
+ | "event";
9
+
10
+ export type AgentGameNodeStatus = "idle" | "running" | "waiting" | "success" | "error";
11
+
12
+ export type AgentGameEdgeKind =
13
+ | "owns"
14
+ | "runs"
15
+ | "uses"
16
+ | "remembers"
17
+ | "emits"
18
+ | "calls"
19
+ | "replies_to"
20
+ | "custom";
21
+
22
+ export type AgentGamePoint = {
23
+ x: number;
24
+ y: number;
25
+ };
26
+
27
+ export type AgentGameNode = {
28
+ id: string;
29
+ kind: AgentGameNodeKind;
30
+ label?: string;
31
+ status?: AgentGameNodeStatus;
32
+ position?: AgentGamePoint;
33
+ metadata?: Record<string, unknown>;
34
+ };
35
+
36
+ export type AgentGameEdge = {
37
+ id?: string;
38
+ source: string;
39
+ target: string;
40
+ kind: AgentGameEdgeKind;
41
+ label?: string;
42
+ metadata?: Record<string, unknown>;
43
+ };
44
+
45
+ export type AgentGameGraph = {
46
+ nodes: AgentGameNode[];
47
+ edges: AgentGameEdge[];
48
+ metadata?: Record<string, unknown>;
49
+ };
50
+
51
+ export type NormalizedAgentGameNode = Required<Pick<AgentGameNode, "id" | "kind" | "label" | "status" | "metadata">> & {
52
+ position?: AgentGamePoint;
53
+ };
54
+
55
+ export type NormalizedAgentGameEdge = Required<Pick<AgentGameEdge, "id" | "source" | "target" | "kind" | "label" | "metadata">>;
56
+
57
+ export type NormalizedAgentGameGraph = {
58
+ nodes: NormalizedAgentGameNode[];
59
+ edges: NormalizedAgentGameEdge[];
60
+ metadata: Record<string, unknown>;
61
+ };
62
+
63
+ export type AgentGameSceneNode = NormalizedAgentGameNode & {
64
+ position: AgentGamePoint;
65
+ };
66
+
67
+ export type AgentGameSceneEdge = NormalizedAgentGameEdge;
68
+
69
+ export type AgentGameScene = {
70
+ nodes: AgentGameSceneNode[];
71
+ edges: AgentGameSceneEdge[];
72
+ metadata: Record<string, unknown>;
73
+ };
74
+
75
+ export type AgentGameSceneOptions = {
76
+ columnGap?: number;
77
+ rowGap?: number;
78
+ origin?: AgentGamePoint;
79
+ layerOrder?: readonly AgentGameNodeKind[];
80
+ };