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

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
@@ -51,13 +51,13 @@ export function Office() {
51
51
 
52
52
  `agent-game-sdk/office` is renderer and framework neutral. `agent-game-sdk/office/react` is the React-only entrypoint.
53
53
 
54
- Browser clients should call an application BFF endpoint. Keep the AgentOS service API key on the server:
54
+ Browser clients should call an application BFF endpoint. Keep the AgentOS service API key on the server; the SDK server client forwards it to Agent Game Runtime:
55
55
 
56
56
  ```ts
57
57
  import { AgentGameRuntimeServerClient } from "agent-game-sdk";
58
58
 
59
59
  const client = new AgentGameRuntimeServerClient({
60
- baseUrl: process.env.AGENTOS_BASE_URL!,
60
+ baseUrl: process.env.AGENT_GAME_RUNTIME_BASE_URL!,
61
61
  apiKey: process.env.AGENTOS_API_KEY!,
62
62
  });
63
63
 
@@ -71,7 +71,7 @@ export async function GET() {
71
71
  Run the SDK office view demo:
72
72
 
73
73
  ```bash
74
- AGENTOS_BASE_URL=http://localhost:3000 AGENTOS_API_KEY=... bun run demo
74
+ AGENT_GAME_RUNTIME_BASE_URL=http://localhost:3107 AGENTOS_API_KEY=... bun run demo
75
75
  ```
76
76
 
77
77
  Open `http://localhost:7357`. Set `PORT=7358` or another value to change the port.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-os-lab/agent-game-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "tsc --noEmit -p tsconfig.json",
16
16
  "demo": "bun --env-file=.env.development --hot demo/server.ts",
17
+ "demo:stop": "bun scripts/stop-demo.ts",
17
18
  "sdk:publish": "bun scripts/publish-agent-game-sdk.ts",
18
19
  "test": "bun test",
19
20
  "typecheck": "tsc --noEmit -p tsconfig.json"
@@ -12,19 +12,12 @@ export type AgentActivityEffectMode =
12
12
 
13
13
  export type AgentActivityEffects = {
14
14
  group: THREE.Group;
15
- typingDots: THREE.Mesh[];
16
- toolBlock: THREE.Mesh;
17
- runningRing: THREE.Mesh;
18
- completionRing: THREE.Mesh;
15
+ typingDots: THREE.Object3D[];
16
+ toolBlock: THREE.Object3D;
17
+ runningRing: THREE.Object3D;
18
+ completionRing: THREE.Object3D;
19
19
  };
20
20
 
21
- const typingDotGeometry = new THREE.SphereGeometry(0.055, 12, 8);
22
- const typingDotMaterial = new THREE.MeshBasicMaterial({ color: 0xe0f2fe });
23
- const toolBlockGeometry = new THREE.BoxGeometry(0.34, 0.22, 0.08);
24
- const toolBlockMaterial = new THREE.MeshBasicMaterial({ color: 0x0f172a });
25
- const ringGeometryCache = new Map<string, THREE.RingGeometry>();
26
- const ringMaterialCache = new Map<number, THREE.MeshBasicMaterial>();
27
-
28
21
  export function resolveActivityEffectMode(
29
22
  agent: Pick<AgentGameOfficeAgent, "activity" | "activityLabel" | "sceneState">,
30
23
  ): AgentActivityEffectMode {
@@ -51,26 +44,20 @@ export function resolveActivityEffectMode(
51
44
  export function createAgentActivityEffects(): AgentActivityEffects {
52
45
  const group = new THREE.Group();
53
46
  const typingDots = [0, 1, 2].map((index) => {
54
- const dot = new THREE.Mesh(
55
- typingDotGeometry,
56
- typingDotMaterial,
57
- );
47
+ const dot = new THREE.Object3D();
58
48
  dot.position.set(-0.18 + index * 0.18, 1.95, 0.18);
59
49
  dot.visible = false;
60
50
  group.add(dot);
61
51
  return dot;
62
52
  });
63
53
 
64
- const toolBlock = new THREE.Mesh(
65
- toolBlockGeometry,
66
- toolBlockMaterial,
67
- );
54
+ const toolBlock = new THREE.Object3D();
68
55
  toolBlock.position.set(0.48, 1.18, 0.18);
69
56
  toolBlock.visible = false;
70
57
  group.add(toolBlock);
71
58
 
72
- const runningRing = createRing(0x38bdf8, 0.48, 0.68);
73
- const completionRing = createRing(0x22c55e, 0.58, 0.82);
59
+ const runningRing = createRing();
60
+ const completionRing = createRing();
74
61
  group.add(runningRing, completionRing);
75
62
 
76
63
  return { group, typingDots, toolBlock, runningRing, completionRing };
@@ -113,44 +100,14 @@ export function updateAgentActivityEffects(
113
100
  }
114
101
  }
115
102
 
116
- function createRing(color: number, innerRadius: number, outerRadius: number): THREE.Mesh {
117
- const ring = new THREE.Mesh(
118
- getRingGeometry(innerRadius, outerRadius),
119
- getRingMaterial(color),
120
- );
103
+ function createRing(): THREE.Object3D {
104
+ const ring = new THREE.Object3D();
121
105
  ring.rotation.x = -Math.PI / 2;
122
106
  ring.position.y = 0.04;
123
107
  ring.visible = false;
124
108
  return ring;
125
109
  }
126
110
 
127
- function getRingGeometry(innerRadius: number, outerRadius: number): THREE.RingGeometry {
128
- const key = `${innerRadius}:${outerRadius}`;
129
- const cached = ringGeometryCache.get(key);
130
- if (cached) {
131
- return cached;
132
- }
133
- const geometry = new THREE.RingGeometry(innerRadius, outerRadius, 32);
134
- ringGeometryCache.set(key, geometry);
135
- return geometry;
136
- }
137
-
138
- function getRingMaterial(color: number): THREE.MeshBasicMaterial {
139
- const cached = ringMaterialCache.get(color);
140
- if (cached) {
141
- return cached;
142
- }
143
- const material = new THREE.MeshBasicMaterial({
144
- color,
145
- transparent: true,
146
- opacity: 0.36,
147
- side: THREE.DoubleSide,
148
- depthWrite: false,
149
- });
150
- ringMaterialCache.set(color, material);
151
- return material;
152
- }
153
-
154
111
  function hideEffects(effects: AgentActivityEffects): void {
155
112
  effects.typingDots.forEach((dot) => {
156
113
  dot.visible = false;
@@ -41,7 +41,7 @@ export class AgentBodyInstancedLayer {
41
41
  }
42
42
  record.mesh.group.updateMatrixWorld(true);
43
43
  resolveAgentBodyPartRenderSpecs(record.renderIndex).forEach((part) => {
44
- const batch = this.ensureBatch(part, records.length);
44
+ const batch = this.ensureBatch(part, records.length * 2);
45
45
  const object = record.mesh[part.key];
46
46
  object.updateMatrixWorld(true);
47
47
  matrixHelper.copy(object.matrixWorld);
@@ -68,7 +68,7 @@ export class AgentBodyInstancedLayer {
68
68
  part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number],
69
69
  requiredCapacity: number,
70
70
  ): AgentBodyBatch {
71
- const key = `${part.key}:${part.width}:${part.height}:${part.depth}:${part.color}`;
71
+ const key = `${part.width}:${part.height}:${part.depth}:${part.color}`;
72
72
  const existing = this.batches.get(key);
73
73
  if (existing && existing.capacity >= requiredCapacity) {
74
74
  return existing;
@@ -0,0 +1,134 @@
1
+ import * as THREE from "three";
2
+
3
+ import type { AgentMeshParts } from "./agent-mesh";
4
+
5
+ export type AgentEffectInstanceRecord = {
6
+ mesh: AgentMeshParts;
7
+ visible: boolean;
8
+ };
9
+
10
+ type EffectBatch = {
11
+ mesh: THREE.InstancedMesh;
12
+ capacity: number;
13
+ count: number;
14
+ };
15
+
16
+ const matrixHelper = new THREE.Matrix4();
17
+
18
+ export class AgentEffectInstancedLayer {
19
+ private readonly scene: THREE.Scene;
20
+ private readonly batches = new Map<string, EffectBatch>();
21
+
22
+ constructor(scene: THREE.Scene) {
23
+ this.scene = scene;
24
+ }
25
+
26
+ update(records: AgentEffectInstanceRecord[]): void {
27
+ this.batches.forEach((batch) => {
28
+ batch.count = 0;
29
+ batch.mesh.count = 0;
30
+ });
31
+
32
+ records.forEach((record) => {
33
+ if (!record.visible) {
34
+ return;
35
+ }
36
+ record.mesh.group.updateMatrixWorld(true);
37
+ this.addObject("glow", record.mesh.activityGlow, records.length);
38
+ record.mesh.activityEffects.typingDots.forEach((dot) => {
39
+ this.addObject("typingDot", dot, records.length * 3);
40
+ });
41
+ this.addObject("toolBlock", record.mesh.activityEffects.toolBlock, records.length);
42
+ this.addObject("runningRing", record.mesh.activityEffects.runningRing, records.length);
43
+ this.addObject("completionRing", record.mesh.activityEffects.completionRing, records.length);
44
+ });
45
+
46
+ this.batches.forEach((batch) => {
47
+ batch.mesh.count = batch.count;
48
+ batch.mesh.visible = batch.count > 0;
49
+ batch.mesh.instanceMatrix.needsUpdate = true;
50
+ });
51
+ }
52
+
53
+ dispose(): void {
54
+ this.batches.forEach((batch) => {
55
+ this.scene.remove(batch.mesh);
56
+ });
57
+ this.batches.clear();
58
+ }
59
+
60
+ private addObject(key: EffectKey, object: THREE.Object3D, capacity: number): void {
61
+ if (!object.visible) {
62
+ return;
63
+ }
64
+ const batch = this.ensureBatch(key, capacity);
65
+ object.updateMatrixWorld(true);
66
+ matrixHelper.copy(object.matrixWorld);
67
+ batch.mesh.setMatrixAt(batch.count, matrixHelper);
68
+ batch.count += 1;
69
+ }
70
+
71
+ private ensureBatch(key: EffectKey, requiredCapacity: number): EffectBatch {
72
+ const existing = this.batches.get(key);
73
+ if (existing && existing.capacity >= requiredCapacity) {
74
+ return existing;
75
+ }
76
+ if (existing) {
77
+ this.scene.remove(existing.mesh);
78
+ }
79
+
80
+ const spec = effectSpecs[key];
81
+ const mesh = new THREE.InstancedMesh(
82
+ spec.geometry,
83
+ spec.material,
84
+ Math.max(requiredCapacity, 1),
85
+ );
86
+ mesh.castShadow = false;
87
+ mesh.receiveShadow = false;
88
+ mesh.name = `agentEffect:${key}`;
89
+ this.scene.add(mesh);
90
+
91
+ const batch = { mesh, capacity: Math.max(requiredCapacity, 1), count: 0 };
92
+ this.batches.set(key, batch);
93
+ return batch;
94
+ }
95
+ }
96
+
97
+ export function createAgentEffectInstancedLayer(scene: THREE.Scene): AgentEffectInstancedLayer {
98
+ return new AgentEffectInstancedLayer(scene);
99
+ }
100
+
101
+ type EffectKey = "glow" | "typingDot" | "toolBlock" | "runningRing" | "completionRing";
102
+
103
+ const effectSpecs: Record<EffectKey, { geometry: THREE.BufferGeometry; material: THREE.Material }> = {
104
+ glow: {
105
+ geometry: new THREE.RingGeometry(0.56, 0.92, 32),
106
+ material: createTransparentMaterial(0x38bdf8, 0.35),
107
+ },
108
+ typingDot: {
109
+ geometry: new THREE.SphereGeometry(0.055, 12, 8),
110
+ material: new THREE.MeshBasicMaterial({ color: 0xe0f2fe }),
111
+ },
112
+ toolBlock: {
113
+ geometry: new THREE.BoxGeometry(0.34, 0.22, 0.08),
114
+ material: new THREE.MeshBasicMaterial({ color: 0x0f172a }),
115
+ },
116
+ runningRing: {
117
+ geometry: new THREE.RingGeometry(0.48, 0.68, 32),
118
+ material: createTransparentMaterial(0x38bdf8, 0.36),
119
+ },
120
+ completionRing: {
121
+ geometry: new THREE.RingGeometry(0.58, 0.82, 32),
122
+ material: createTransparentMaterial(0x22c55e, 0.36),
123
+ },
124
+ };
125
+
126
+ function createTransparentMaterial(color: number, opacity: number): THREE.MeshBasicMaterial {
127
+ return new THREE.MeshBasicMaterial({
128
+ color,
129
+ transparent: true,
130
+ opacity,
131
+ side: THREE.DoubleSide,
132
+ depthWrite: false,
133
+ });
134
+ }
@@ -21,23 +21,14 @@ export type AgentMeshParts = {
21
21
  rightLeg: THREE.Object3D;
22
22
  leftFoot: THREE.Object3D;
23
23
  rightFoot: THREE.Object3D;
24
- activityGlow: THREE.Mesh;
24
+ activityGlow: THREE.Object3D;
25
25
  activityEffects: AgentActivityEffects;
26
26
  };
27
27
 
28
- const agentColors = [0x2563eb, 0xdc2626, 0x059669, 0xd97706, 0x7c3aed, 0x0891b2];
29
- const skinColors = [0xffdbac, 0xf5cba7, 0xd4a574, 0xc68642];
30
- const hairColors = [0x3f2a1d, 0x111827, 0x7c2d12, 0x4b5563];
28
+ const agentColors = [0x2563eb, 0xdc2626, 0x059669];
29
+ const skinColors = [0xffdbac, 0xd4a574];
30
+ const hairColors = [0x3f2a1d, 0x111827];
31
31
  export const AGENT_MESH_SCALE = 1.15;
32
- const activityGlowGeometry = new THREE.RingGeometry(0.56, 0.92, 32);
33
- const activityGlowMaterial = new THREE.MeshBasicMaterial({
34
- color: 0x38bdf8,
35
- transparent: true,
36
- opacity: 0.35,
37
- side: THREE.DoubleSide,
38
- depthWrite: false,
39
- });
40
-
41
32
  export type AgentBodyPartKey =
42
33
  | "body"
43
34
  | "leftLeg"
@@ -133,11 +124,8 @@ function addPart(
133
124
  return part;
134
125
  }
135
126
 
136
- function createActivityGlow(): THREE.Mesh {
137
- const glow = new THREE.Mesh(
138
- activityGlowGeometry,
139
- activityGlowMaterial,
140
- );
127
+ function createActivityGlow(): THREE.Object3D {
128
+ const glow = new THREE.Object3D();
141
129
  glow.rotation.x = -Math.PI / 2;
142
130
  glow.position.y = 0.03;
143
131
  glow.visible = false;
@@ -9,6 +9,7 @@ import type {
9
9
  } from "../../core/types";
10
10
  import { applyAgentPose, updateAgentMotion } from "./agent-animation";
11
11
  import { createAgentBodyInstancedLayer } from "./agent-body-instancing";
12
+ import { createAgentEffectInstancedLayer } from "./agent-effect-instancing";
12
13
  import { resolveAgentFacingTarget, resolveAgentPosition } from "./agent-layout";
13
14
  import {
14
15
  type AgentLabelRecord,
@@ -113,6 +114,7 @@ export function mountThreeAgentGameOffice(
113
114
  addLights(scene);
114
115
  buildOfficeScene(scene);
115
116
  const agentBodyLayer = createAgentBodyInstancedLayer(scene);
117
+ const agentEffectLayer = createAgentEffectInstancedLayer(scene);
116
118
 
117
119
  const agents = new Map<string, AgentMeshRecord>();
118
120
  let frameId = 0;
@@ -193,6 +195,10 @@ export function mountThreeAgentGameOffice(
193
195
  renderIndex: record.renderIndex,
194
196
  visible: record.agent.sceneState !== "offline",
195
197
  })));
198
+ agentEffectLayer.update(Array.from(agents.values()).map((record) => ({
199
+ mesh: record.mesh,
200
+ visible: record.agent.sceneState !== "offline",
201
+ })));
196
202
  renderer.render(scene, camera);
197
203
  labelsDirty = false;
198
204
  frameId = requestAnimationFrame(animate);
@@ -245,6 +251,7 @@ export function mountThreeAgentGameOffice(
245
251
  });
246
252
  agents.clear();
247
253
  agentBodyLayer.dispose();
254
+ agentEffectLayer.dispose();
248
255
  disposeObject(scene);
249
256
  renderer.dispose();
250
257
  root.remove();
@@ -158,7 +158,7 @@ export class AgentGameRuntimeBrowserClient {
158
158
  signal: options.signal,
159
159
  });
160
160
  if (!response.ok) {
161
- throw new Error(`Game runtime token request failed with status ${response.status}.`);
161
+ throw new Error(await formatGameRuntimeTokenRequestError(response));
162
162
  }
163
163
  const body = await response.json() as GameRuntimeTokenResponse;
164
164
  if (!isGameRuntimeTokenResponse(body)) {
@@ -217,7 +217,7 @@ export class AgentGameRuntimeServerClient {
217
217
  signal: options.signal,
218
218
  });
219
219
  if (!response.ok) {
220
- throw new Error(`Game runtime token request failed with status ${response.status}.`);
220
+ throw new Error(await formatGameRuntimeTokenRequestError(response));
221
221
  }
222
222
  const body = await response.json() as GameRuntimeTokenResponse;
223
223
  if (!isGameRuntimeTokenResponse(body)) {
@@ -377,6 +377,31 @@ function boundFetch(): typeof fetch {
377
377
  return globalThis.fetch.bind(globalThis);
378
378
  }
379
379
 
380
+ async function formatGameRuntimeTokenRequestError(response: Response): Promise<string> {
381
+ const detail = await readErrorDetail(response);
382
+ return `Game runtime token request failed with status ${response.status}${detail ? `: ${detail}` : ""}.`;
383
+ }
384
+
385
+ async function readErrorDetail(response: Response): Promise<string> {
386
+ try {
387
+ const body = await response.clone().json() as unknown;
388
+ if (!body || typeof body !== "object") {
389
+ return "";
390
+ }
391
+ const error = (body as { error?: unknown }).error;
392
+ if (typeof error === "string") {
393
+ return error;
394
+ }
395
+ if (error && typeof error === "object") {
396
+ const message = (error as { message?: unknown }).message;
397
+ return typeof message === "string" ? message : "";
398
+ }
399
+ } catch {
400
+ return "";
401
+ }
402
+ return "";
403
+ }
404
+
380
405
  function resolveHeaders(
381
406
  value: AgentGameRuntimeBrowserClientOptions["headers"] | AgentGameRuntimeServerClientOptions["headers"],
382
407
  ): HeadersInit | undefined {