@vgai/sdk 0.5.0 → 0.5.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/package.json +2 -2
- package/src/account.ts +5 -4
- package/src/cinematic/capabilities-operations.ts +3 -3
- package/src/cinematic/cue-operations.ts +11 -11
- package/src/cinematic/gsap-operations.ts +5 -5
- package/src/cinematic/index.ts +3 -3
- package/src/cinematic/preview-operations.ts +10 -10
- package/src/cinematic/preview-transport.ts +2 -2
- package/src/cinematic/render-operations.ts +15 -15
- package/src/cinematic/render-transport.ts +2 -2
- package/src/cinematic/theatre-operations.ts +17 -17
- package/src/editor/camera-operations.ts +7 -7
- package/src/editor/console-operations.ts +5 -5
- package/src/editor/hierarchy-operations.ts +5 -5
- package/src/editor/index.ts +3 -3
- package/src/editor/open-operations.ts +10 -10
- package/src/editor/screenshot-operations.ts +5 -5
- package/src/editor/selection-operations.ts +7 -7
- package/src/editor/session-operations.ts +3 -3
- package/src/editor/source-location-operations.ts +6 -6
- package/src/editor/transport.ts +24 -11
- package/src/errors.ts +8 -8
- package/src/generations.ts +12 -1
- package/src/http/http-projection.ts +26 -26
- package/src/index.ts +22 -30
- package/src/mcp/index.ts +2 -2
- package/src/mcp/mcp-projection.ts +25 -25
- package/src/mcp/mcp-server.ts +2 -2
- package/src/operations.ts +5 -5
- package/src/play/control-operations.ts +10 -10
- package/src/play/debug-command-operations.ts +12 -12
- package/src/play/index.ts +3 -3
- package/src/play/input-operations.ts +10 -10
- package/src/play/lifecycle-operations.ts +11 -11
- package/src/play/log-operations.ts +7 -7
- package/src/play/run-ticks-operations.ts +8 -8
- package/src/play/state-operations.ts +9 -9
- package/src/play/status-operations.ts +7 -7
- package/src/play/transport.ts +93 -22
- package/src/project/asset-operations.ts +8 -8
- package/src/project/component-operations.ts +10 -10
- package/src/project/discovery-operations.ts +9 -9
- package/src/project/entity-operations.ts +8 -8
- package/src/project/index.ts +4 -4
- package/src/project/input-map-operations.ts +9 -9
- package/src/project/inspection-operation.ts +3 -3
- package/src/project/manifest-operations.ts +9 -9
- package/src/project/scene-operations.ts +14 -14
- package/src/project/shared.ts +17 -17
- package/src/project-tool-catalog.ts +21 -5
- package/src/registry.ts +39 -39
- package/src/render/render-cinematic.ts +24 -1
- package/src/tools.ts +19 -12
- package/src/types.ts +4 -4
package/src/play/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* B4 — `play.*` operations
|
|
3
3
|
* (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B4). Registered
|
|
4
|
-
* separately from B1's `
|
|
4
|
+
* separately from B1's `registerBuiltinTools` (`../operations.ts`),
|
|
5
5
|
* B2's `registerProjectOperations` (`../project/index.ts`), and B3's
|
|
6
6
|
* `registerEditorOperations` (`../editor/index.ts`) — the default
|
|
7
7
|
* `operations` singleton (`../index.ts`) calls all four.
|
|
@@ -43,7 +43,7 @@ export {
|
|
|
43
43
|
withPlayTimeout,
|
|
44
44
|
} from './transport.js';
|
|
45
45
|
|
|
46
|
-
import type {
|
|
46
|
+
import type { ToolRegistry } from '../registry.js';
|
|
47
47
|
import { registerControlOperations } from './control-operations.js';
|
|
48
48
|
import { registerDebugCommandOperations } from './debug-command-operations.js';
|
|
49
49
|
import { registerInputOperations } from './input-operations.js';
|
|
@@ -54,7 +54,7 @@ import { registerStateOperations } from './state-operations.js';
|
|
|
54
54
|
import { registerStatusOperations } from './status-operations.js';
|
|
55
55
|
|
|
56
56
|
/** Register every B4 `play.*` operation onto `registry`. */
|
|
57
|
-
export function registerPlayOperations(registry:
|
|
57
|
+
export function registerPlayOperations(registry: ToolRegistry): void {
|
|
58
58
|
registerLifecycleOperations(registry);
|
|
59
59
|
registerStatusOperations(registry);
|
|
60
60
|
registerLogOperations(registry);
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
60
|
import { z } from 'zod';
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
61
|
+
import { ToolError } from '../errors.js';
|
|
62
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
63
63
|
import type { InputInjectionRequest, PlayCommandResult } from './transport.js';
|
|
64
64
|
import {
|
|
65
65
|
getPlayTransport,
|
|
@@ -205,22 +205,22 @@ function throwForInjectFailure(result: PlayCommandResult): never {
|
|
|
205
205
|
const marker = relayErrorMarker(result);
|
|
206
206
|
const message = result.error ?? 'inject-input command failed';
|
|
207
207
|
if (marker?.code === 'INPUT_ACTION_NOT_FOUND') {
|
|
208
|
-
throw new
|
|
208
|
+
throw new ToolError('INPUT_ACTION_NOT_FOUND', message, {
|
|
209
209
|
registered: relayStringArray(marker.data['registered']),
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
if (marker?.code === 'TICK_ALREADY_PASSED') {
|
|
213
|
-
throw new
|
|
213
|
+
throw new ToolError('TICK_ALREADY_PASSED', message, {
|
|
214
214
|
currentTick: typeof marker.data['currentTick'] === 'number' ? marker.data['currentTick'] : 0,
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
if (marker?.code === 'DEBUG_INPUT_WORLD_NOT_FOUND') {
|
|
218
|
-
throw new
|
|
218
|
+
throw new ToolError('INPUT_WORLD_NOT_FOUND', message, {
|
|
219
219
|
worldId: typeof marker.data['worldId'] === 'string' ? marker.data['worldId'] : '',
|
|
220
220
|
registered: relayStringArray(marker.data['registered']),
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
-
throw new
|
|
223
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
const PlayInputInjectResult = z.object({
|
|
@@ -234,7 +234,7 @@ const PlayInputInjectResult = z.object({
|
|
|
234
234
|
),
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
export const playInputInject =
|
|
237
|
+
export const playInputInject = defineTool({
|
|
238
238
|
name: 'play.input.inject',
|
|
239
239
|
summary: 'Inject synthetic input into a running play session through the normal action path.',
|
|
240
240
|
description:
|
|
@@ -289,7 +289,7 @@ export const playInputInject = defineOperation({
|
|
|
289
289
|
'play.input.inject',
|
|
290
290
|
).catch(() => undefined);
|
|
291
291
|
if (!result) {
|
|
292
|
-
throw new
|
|
292
|
+
throw new ToolError(
|
|
293
293
|
'INPUT_INJECTION_UNSUPPORTED',
|
|
294
294
|
'The connected editor page predates the inject-input relay command — reload it.',
|
|
295
295
|
{},
|
|
@@ -303,7 +303,7 @@ export const playInputInject = defineOperation({
|
|
|
303
303
|
| undefined;
|
|
304
304
|
if (outcome?.delivered === false) {
|
|
305
305
|
const reason = outcome.reason ?? 'input-gated';
|
|
306
|
-
throw new
|
|
306
|
+
throw new ToolError('INPUT_GATED', reason, { reason });
|
|
307
307
|
}
|
|
308
308
|
return outcome?.scheduled === true
|
|
309
309
|
? { ok: true as const, scheduled: true }
|
|
@@ -311,6 +311,6 @@ export const playInputInject = defineOperation({
|
|
|
311
311
|
},
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
export function registerInputOperations(registry:
|
|
314
|
+
export function registerInputOperations(registry: ToolRegistry): void {
|
|
315
315
|
registry.register(playInputInject);
|
|
316
316
|
}
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { z } from 'zod';
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
38
|
+
import { ToolError } from '../errors.js';
|
|
39
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
40
40
|
import {
|
|
41
41
|
getPlayTransport,
|
|
42
42
|
PLAY_COMMAND_TIMEOUT_MS,
|
|
@@ -74,7 +74,7 @@ async function relayAndCheck(
|
|
|
74
74
|
error: err instanceof Error ? err.message : String(err),
|
|
75
75
|
}));
|
|
76
76
|
if (!result.ok) {
|
|
77
|
-
throw new
|
|
77
|
+
throw new ToolError('COMMAND_FAILED', result.error ?? `${op} command failed`, {
|
|
78
78
|
message: result.error ?? `${op} command failed`,
|
|
79
79
|
});
|
|
80
80
|
}
|
|
@@ -106,7 +106,7 @@ const PlayStartInput = z.object({
|
|
|
106
106
|
),
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
export const playStart =
|
|
109
|
+
export const playStart = defineTool({
|
|
110
110
|
name: 'play.start',
|
|
111
111
|
summary: 'Start (or restart) play mode in a connected editor session and wait for readiness.',
|
|
112
112
|
description:
|
|
@@ -137,7 +137,7 @@ export const playStart = defineOperation({
|
|
|
137
137
|
);
|
|
138
138
|
} catch (err) {
|
|
139
139
|
if (err instanceof PlayTimeoutError) {
|
|
140
|
-
throw new
|
|
140
|
+
throw new ToolError(
|
|
141
141
|
'PLAY_START_TIMEOUT',
|
|
142
142
|
`The runtime did not report readiness within ${timeoutMs}ms.`,
|
|
143
143
|
{ timeoutMs },
|
|
@@ -146,7 +146,7 @@ export const playStart = defineOperation({
|
|
|
146
146
|
throw err;
|
|
147
147
|
}
|
|
148
148
|
if (!result.ok) {
|
|
149
|
-
throw new
|
|
149
|
+
throw new ToolError('COMMAND_FAILED', result.error ?? 'play command failed', {
|
|
150
150
|
message: result.error ?? 'play command failed',
|
|
151
151
|
});
|
|
152
152
|
}
|
|
@@ -160,7 +160,7 @@ export const playStart = defineOperation({
|
|
|
160
160
|
|
|
161
161
|
const NoInput = z.object({});
|
|
162
162
|
|
|
163
|
-
export const playPause =
|
|
163
|
+
export const playPause = defineTool({
|
|
164
164
|
name: 'play.pause',
|
|
165
165
|
summary: 'Pause the running play session in a connected editor.',
|
|
166
166
|
description: 'Relays {type:"pause"} through POST /__editor/command (already-handled case).',
|
|
@@ -184,7 +184,7 @@ export const playPause = defineOperation({
|
|
|
184
184
|
},
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
export const playResume =
|
|
187
|
+
export const playResume = defineTool({
|
|
188
188
|
name: 'play.resume',
|
|
189
189
|
summary: 'Resume a paused play session in a connected editor.',
|
|
190
190
|
description: 'Relays {type:"resume"} through POST /__editor/command (already-handled case).',
|
|
@@ -208,7 +208,7 @@ export const playResume = defineOperation({
|
|
|
208
208
|
},
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
export const playStop =
|
|
211
|
+
export const playStop = defineTool({
|
|
212
212
|
name: 'play.stop',
|
|
213
213
|
summary: 'Stop play mode in a connected editor and clean up the launched runtime.',
|
|
214
214
|
description:
|
|
@@ -238,7 +238,7 @@ export const playStop = defineOperation({
|
|
|
238
238
|
},
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
-
export const playFrameStep =
|
|
241
|
+
export const playFrameStep = defineTool({
|
|
242
242
|
name: 'play.frameStep',
|
|
243
243
|
summary: 'Advance a paused play session by exactly one frame.',
|
|
244
244
|
description: 'Relays {type:"step"} through POST /__editor/command (already-handled case).',
|
|
@@ -262,7 +262,7 @@ export const playFrameStep = defineOperation({
|
|
|
262
262
|
},
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
-
export function registerLifecycleOperations(registry:
|
|
265
|
+
export function registerLifecycleOperations(registry: ToolRegistry): void {
|
|
266
266
|
registry.register(playStart);
|
|
267
267
|
registry.register(playPause);
|
|
268
268
|
registry.register(playResume);
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { readdirSync } from 'node:fs';
|
|
44
44
|
import { join } from 'node:path';
|
|
45
45
|
import { z } from 'zod';
|
|
46
|
-
import {
|
|
46
|
+
import { ToolError } from '../errors.js';
|
|
47
47
|
import {
|
|
48
48
|
FILE_NOT_FOUND_ERROR,
|
|
49
49
|
NO_PROJECT_ROOT_ERROR,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
requireProjectRoot,
|
|
53
53
|
resolveProjectPath,
|
|
54
54
|
} from '../project/shared.js';
|
|
55
|
-
import {
|
|
55
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
56
56
|
import {
|
|
57
57
|
getPlayTransport,
|
|
58
58
|
PLAY_READ_TIMEOUT_MS,
|
|
@@ -81,7 +81,7 @@ const PlayLogDiscoverResult = z.object({
|
|
|
81
81
|
.describe('Filenames (e.g. "play-2026-07-11T10-00-00.jsonl"), oldest first (name-sorted).'),
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
export const playLogDiscover =
|
|
84
|
+
export const playLogDiscover = defineTool({
|
|
85
85
|
name: 'play.log.discover',
|
|
86
86
|
summary: "List every persisted play-mode log file under the project's logs/ directory.",
|
|
87
87
|
description:
|
|
@@ -189,7 +189,7 @@ function parseLogLine(
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
export const playLogRead =
|
|
192
|
+
export const playLogRead = defineTool({
|
|
193
193
|
name: 'play.log.read',
|
|
194
194
|
summary: 'Read entries from a persisted play-mode log file.',
|
|
195
195
|
description:
|
|
@@ -238,7 +238,7 @@ const PlayLogFollowResult = z.object({
|
|
|
238
238
|
.describe('Live count of entries buffered for the active play session.'),
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
-
export const playLogFollow =
|
|
241
|
+
export const playLogFollow = defineTool({
|
|
242
242
|
name: 'play.log.follow',
|
|
243
243
|
summary: "Describe how to poll-follow the currently active play session's log.",
|
|
244
244
|
description:
|
|
@@ -262,7 +262,7 @@ export const playLogFollow = defineOperation({
|
|
|
262
262
|
'play.log.follow',
|
|
263
263
|
).catch(() => undefined);
|
|
264
264
|
if (!metadata) {
|
|
265
|
-
throw new
|
|
265
|
+
throw new ToolError(
|
|
266
266
|
'PLAY_LOG_FOLLOW_UNAVAILABLE',
|
|
267
267
|
'The connected editor session did not answer the log-entries read in time.',
|
|
268
268
|
{},
|
|
@@ -272,7 +272,7 @@ export const playLogFollow = defineOperation({
|
|
|
272
272
|
},
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
export function registerLogOperations(registry:
|
|
275
|
+
export function registerLogOperations(registry: ToolRegistry): void {
|
|
276
276
|
registry.register(playLogDiscover);
|
|
277
277
|
registry.register(playLogRead);
|
|
278
278
|
registry.register(playLogFollow);
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { z } from 'zod';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { ToolError } from '../errors.js';
|
|
24
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
25
25
|
import {
|
|
26
26
|
getPlayTransport,
|
|
27
27
|
PLAY_COMMAND_TIMEOUT_MS,
|
|
@@ -81,7 +81,7 @@ const PlayRunTicksInput = z.object({
|
|
|
81
81
|
|
|
82
82
|
const PlayRunTicksResult = z.object({ ok: z.literal(true) });
|
|
83
83
|
|
|
84
|
-
export const playRunTicks =
|
|
84
|
+
export const playRunTicks = defineTool({
|
|
85
85
|
name: 'play.runTicks',
|
|
86
86
|
summary: 'Synchronously fast-forward a running play session by n fixed gameplay ticks.',
|
|
87
87
|
description:
|
|
@@ -115,7 +115,7 @@ export const playRunTicks = defineOperation({
|
|
|
115
115
|
'play.runTicks',
|
|
116
116
|
).catch(() => undefined);
|
|
117
117
|
if (!result) {
|
|
118
|
-
throw new
|
|
118
|
+
throw new ToolError(
|
|
119
119
|
'RUN_TICKS_UNSUPPORTED',
|
|
120
120
|
'The connected editor page predates the run-ticks relay command — reload it.',
|
|
121
121
|
{},
|
|
@@ -125,17 +125,17 @@ export const playRunTicks = defineOperation({
|
|
|
125
125
|
const marker = relayErrorMarker(result);
|
|
126
126
|
const message = result.error ?? 'run-ticks command failed';
|
|
127
127
|
if (marker?.code === 'RUN_TICKS_UNAVAILABLE') {
|
|
128
|
-
throw new
|
|
128
|
+
throw new ToolError('RUN_TICKS_UNAVAILABLE', message, {});
|
|
129
129
|
}
|
|
130
130
|
if (marker?.code === 'RUN_TICKS_PAUSED') {
|
|
131
|
-
throw new
|
|
131
|
+
throw new ToolError('RUN_TICKS_PAUSED', message, {});
|
|
132
132
|
}
|
|
133
|
-
throw new
|
|
133
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
134
134
|
}
|
|
135
135
|
return { ok: true as const };
|
|
136
136
|
},
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
export function registerRunTicksOperations(registry:
|
|
139
|
+
export function registerRunTicksOperations(registry: ToolRegistry): void {
|
|
140
140
|
registry.register(playRunTicks);
|
|
141
141
|
}
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { z } from 'zod';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { ToolError } from '../errors.js';
|
|
25
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
26
26
|
import {
|
|
27
27
|
getPlayTransport,
|
|
28
28
|
PLAY_READ_TIMEOUT_MS,
|
|
@@ -61,14 +61,14 @@ const COMMAND_FAILED_ERROR = {
|
|
|
61
61
|
function throwStateRelayFailure(result: PlayCommandResult, label: string): never {
|
|
62
62
|
const marker = relayErrorMarker(result);
|
|
63
63
|
if (marker?.code === 'STATE_PROVIDER_NOT_FOUND') {
|
|
64
|
-
throw new
|
|
64
|
+
throw new ToolError(
|
|
65
65
|
'STATE_PROVIDER_NOT_FOUND',
|
|
66
66
|
result.error ?? 'state provider not found',
|
|
67
67
|
{ registered: relayStringArray(marker.data['registered']) },
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
const message = result.error ?? `${label} command failed`;
|
|
71
|
-
throw new
|
|
71
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +92,7 @@ const PlayGameplayStateInspectResult = z.object({
|
|
|
92
92
|
.describe('The game-provided state snapshot, or null when the game exposes none.'),
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
export const playGameplayStateInspect =
|
|
95
|
+
export const playGameplayStateInspect = defineTool({
|
|
96
96
|
name: 'play.gameplayState.inspect',
|
|
97
97
|
summary: 'Inspect game-registered state providers from a running play session.',
|
|
98
98
|
description:
|
|
@@ -126,7 +126,7 @@ export const playGameplayStateInspect = defineOperation({
|
|
|
126
126
|
'play.gameplayState.inspect',
|
|
127
127
|
).catch(() => undefined);
|
|
128
128
|
if (result === undefined) {
|
|
129
|
-
throw new
|
|
129
|
+
throw new ToolError(
|
|
130
130
|
'GAMEPLAY_STATE_UNSUPPORTED',
|
|
131
131
|
'The connected editor page predates the gameplay-state relay commands — reload it.',
|
|
132
132
|
{},
|
|
@@ -158,7 +158,7 @@ const PlayGameplayStateListResult = z.object({
|
|
|
158
158
|
.describe('Every registered state provider of the running game, with its honesty tier.'),
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
export const playGameplayStateList =
|
|
161
|
+
export const playGameplayStateList = defineTool({
|
|
162
162
|
name: 'play.gameplayState.list',
|
|
163
163
|
summary: 'List the state providers a running play session registers, with honesty tiers.',
|
|
164
164
|
description:
|
|
@@ -190,7 +190,7 @@ export const playGameplayStateList = defineOperation({
|
|
|
190
190
|
'play.gameplayState.list',
|
|
191
191
|
).catch(() => undefined);
|
|
192
192
|
if (result === undefined) {
|
|
193
|
-
throw new
|
|
193
|
+
throw new ToolError(
|
|
194
194
|
'GAMEPLAY_STATE_UNSUPPORTED',
|
|
195
195
|
'The connected editor page predates the gameplay-state relay commands — reload it.',
|
|
196
196
|
{},
|
|
@@ -204,7 +204,7 @@ export const playGameplayStateList = defineOperation({
|
|
|
204
204
|
},
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
export function registerStateOperations(registry:
|
|
207
|
+
export function registerStateOperations(registry: ToolRegistry): void {
|
|
208
208
|
registry.register(playGameplayStateInspect);
|
|
209
209
|
registry.register(playGameplayStateList);
|
|
210
210
|
}
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { z } from 'zod';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { ToolError } from '../errors.js';
|
|
24
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
25
25
|
import {
|
|
26
26
|
getPlayTransport,
|
|
27
27
|
PLAY_READ_TIMEOUT_MS,
|
|
@@ -73,7 +73,7 @@ const PlayStatusResult = z.object({
|
|
|
73
73
|
.describe('Currently applied simulation time-scale, or null when never successfully set.'),
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
export const playStatus =
|
|
76
|
+
export const playStatus = defineTool({
|
|
77
77
|
name: 'play.status',
|
|
78
78
|
summary: 'Read the current play-mode status (running/paused/stopped, seed, time-scale).',
|
|
79
79
|
description:
|
|
@@ -97,7 +97,7 @@ export const playStatus = defineOperation({
|
|
|
97
97
|
'play.status',
|
|
98
98
|
).catch(() => undefined);
|
|
99
99
|
if (!status) {
|
|
100
|
-
throw new
|
|
100
|
+
throw new ToolError(
|
|
101
101
|
'PLAY_STATUS_UNAVAILABLE',
|
|
102
102
|
'The connected editor session did not answer the status read in time.',
|
|
103
103
|
{},
|
|
@@ -120,7 +120,7 @@ const PlayActiveWorldInspectResult = z.object({
|
|
|
120
120
|
worldKind: z.string(),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
export const playActiveWorldInspect =
|
|
123
|
+
export const playActiveWorldInspect = defineTool({
|
|
124
124
|
name: 'play.activeWorld.inspect',
|
|
125
125
|
summary: 'Inspect the active world of a running play session (id/kind).',
|
|
126
126
|
description:
|
|
@@ -144,7 +144,7 @@ export const playActiveWorldInspect = defineOperation({
|
|
|
144
144
|
'play.activeWorld.inspect',
|
|
145
145
|
).catch(() => undefined);
|
|
146
146
|
if (!world) {
|
|
147
|
-
throw new
|
|
147
|
+
throw new ToolError(
|
|
148
148
|
'ACTIVE_WORLD_UNAVAILABLE',
|
|
149
149
|
"The connected editor's wire protocol does not carry active-world data today.",
|
|
150
150
|
{},
|
|
@@ -154,7 +154,7 @@ export const playActiveWorldInspect = defineOperation({
|
|
|
154
154
|
},
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
export function registerStatusOperations(registry:
|
|
157
|
+
export function registerStatusOperations(registry: ToolRegistry): void {
|
|
158
158
|
registry.register(playStatus);
|
|
159
159
|
registry.register(playActiveWorldInspect);
|
|
160
160
|
}
|
package/src/play/transport.ts
CHANGED
|
@@ -78,9 +78,9 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
|
78
78
|
import { homedir } from 'node:os';
|
|
79
79
|
import { join, resolve } from 'node:path';
|
|
80
80
|
import { z } from 'zod';
|
|
81
|
-
import {
|
|
82
|
-
import type {
|
|
83
|
-
import type {
|
|
81
|
+
import { ToolError } from '../errors.js';
|
|
82
|
+
import type { ToolErrorDefinition } from '../registry.js';
|
|
83
|
+
import type { ToolContext } from '../types.js';
|
|
84
84
|
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
// Named timeouts
|
|
@@ -92,8 +92,35 @@ export const PLAY_SESSION_DISCOVERY_TIMEOUT_MS = 2000;
|
|
|
92
92
|
export const PLAY_PROBE_TIMEOUT_MS = 1500;
|
|
93
93
|
/** Bound on a plain read (status, log-entries count, ...). */
|
|
94
94
|
export const PLAY_READ_TIMEOUT_MS = 3000;
|
|
95
|
-
/**
|
|
96
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Bound on a relayed pause/resume/stop/frame-step command.
|
|
97
|
+
*
|
|
98
|
+
* The relay's own generic window is `COMMAND_TIMEOUT_MS = 5000`
|
|
99
|
+
* (`packages/editor/server/editor-server.ts`), and this bound must sit ABOVE
|
|
100
|
+
* it for exactly the reason `PLAY_START_TIMEOUT_MS` documents below: the
|
|
101
|
+
* client's clock starts before the request has even reached the server, so a
|
|
102
|
+
* client bound EQUAL to the server's can only ever fire first. It did. A
|
|
103
|
+
* command the editor merely answered slowly came back as
|
|
104
|
+
* `AbortSignal.timeout` → `postJson` returning undefined → "Command relay
|
|
105
|
+
* unreachable." — which reads as "no editor is running" and sends the caller
|
|
106
|
+
* to restart a perfectly healthy session. The server's own honest verdict
|
|
107
|
+
* ("editor connected but did not respond", `timedOut: true`) could never be
|
|
108
|
+
* delivered, because nobody was still listening when it was written.
|
|
109
|
+
*
|
|
110
|
+
* Caught by 39-play-sdk-live's AC-A2.3/AC-A2.5, where every
|
|
111
|
+
* `play.gameplayState.inspect` poll reported an unreachable relay while
|
|
112
|
+
* `play.input.inject` on the same session had just succeeded.
|
|
113
|
+
*
|
|
114
|
+
* 8000 was not enough margin. The server's generic window is 5000
|
|
115
|
+
* (`COMMAND_TIMEOUT_MS`), so 8000 left 3s to cover the request reaching the
|
|
116
|
+
* server, the SSE hop to the browser, the result hop back, and the response —
|
|
117
|
+
* and on a loaded machine that is not enough: the 2026-07-27 nightly saw the
|
|
118
|
+
* same misleading "Command relay unreachable." on a session that was fine.
|
|
119
|
+
* Waiting longer costs a HEALTHY call nothing (it returns as soon as the
|
|
120
|
+
* browser answers); the bound exists only to cap the pathological path, and
|
|
121
|
+
* on that path the server's own verdict is the message worth having.
|
|
122
|
+
*/
|
|
123
|
+
export const PLAY_COMMAND_TIMEOUT_MS = 20_000;
|
|
97
124
|
/**
|
|
98
125
|
* Bound on `play.start`. Mirrors the real relay's own
|
|
99
126
|
* `PLAY_COMMAND_TIMEOUT_MS = 120_000` (`packages/editor/server/editor-server.ts:385`)
|
|
@@ -145,6 +172,8 @@ export interface PlaySessionInfo {
|
|
|
145
172
|
port: number;
|
|
146
173
|
project: string | null;
|
|
147
174
|
pid: number | null;
|
|
175
|
+
/** Registry discriminator. `kind: 'e2e'` is standalone and cannot host editor-relayed play control. */
|
|
176
|
+
kind?: string;
|
|
148
177
|
/** Exact explicitly targeted editor origin/base URL, including protocol and host. */
|
|
149
178
|
url?: string;
|
|
150
179
|
}
|
|
@@ -364,6 +393,7 @@ interface RegistrySessionEntry {
|
|
|
364
393
|
port: number;
|
|
365
394
|
pid: number;
|
|
366
395
|
startedAt: string;
|
|
396
|
+
kind?: string;
|
|
367
397
|
}
|
|
368
398
|
|
|
369
399
|
function isRegistrySessionEntry(v: unknown): v is RegistrySessionEntry {
|
|
@@ -373,7 +403,8 @@ function isRegistrySessionEntry(v: unknown): v is RegistrySessionEntry {
|
|
|
373
403
|
(typeof s['project'] === 'string' || s['project'] === null) &&
|
|
374
404
|
typeof s['port'] === 'number' &&
|
|
375
405
|
typeof s['pid'] === 'number' &&
|
|
376
|
-
typeof s['startedAt'] === 'string'
|
|
406
|
+
typeof s['startedAt'] === 'string' &&
|
|
407
|
+
(s['kind'] === undefined || typeof s['kind'] === 'string')
|
|
377
408
|
);
|
|
378
409
|
}
|
|
379
410
|
|
|
@@ -393,7 +424,10 @@ function readRegisteredSessions(): RegistrySessionEntry[] {
|
|
|
393
424
|
try {
|
|
394
425
|
const raw: unknown = JSON.parse(readFileSync(registryFile, 'utf8'));
|
|
395
426
|
return Array.isArray(raw)
|
|
396
|
-
? raw
|
|
427
|
+
? raw
|
|
428
|
+
.filter(isRegistrySessionEntry)
|
|
429
|
+
.filter((s) => s.kind !== 'e2e')
|
|
430
|
+
.filter((s) => pidAlive(s.pid))
|
|
397
431
|
: [];
|
|
398
432
|
} catch {
|
|
399
433
|
return [];
|
|
@@ -410,11 +444,39 @@ async function fetchJson(url: string, timeoutMs: number): Promise<unknown | unde
|
|
|
410
444
|
}
|
|
411
445
|
}
|
|
412
446
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
447
|
+
/** Either a response came back, or it didn't and `reason` says why.
|
|
448
|
+
*
|
|
449
|
+
* This used to be `… | undefined`, and the single caller turned every
|
|
450
|
+
* `undefined` into the same sentence: "Command relay unreachable." That
|
|
451
|
+
* sentence names a CONNECTION fault, but the `catch` it came from covers a
|
|
452
|
+
* refused socket, a DNS failure, an aborted request, and a body that never
|
|
453
|
+
* arrived — four different diagnoses wearing one message. The cost is on the
|
|
454
|
+
* record: the 2026-07-27 nightly reported an unreachable relay, the message
|
|
455
|
+
* read as "no editor is running", the fix went into timeout constants
|
|
456
|
+
* (`PLAY_COMMAND_TIMEOUT_MS`, twice), and the run stayed red because the
|
|
457
|
+
* actual cause was never in the log. The relay's SERVER always answers within
|
|
458
|
+
* its per-type window with an explicit verdict (`relayCommand`,
|
|
459
|
+
* editor-server.ts) — so when nothing comes back at all, WHICH transport
|
|
460
|
+
* fault it was is the entire diagnosis, and it is the one thing we were
|
|
461
|
+
* throwing away. */
|
|
462
|
+
type PostOutcome =
|
|
463
|
+
| { delivered: true; status: number; json: unknown }
|
|
464
|
+
| { delivered: false; reason: string };
|
|
465
|
+
|
|
466
|
+
/** Turn a `fetch` rejection into something a caller can act on. Undici hangs
|
|
467
|
+
* the useful part off `cause.code` (`ECONNREFUSED`, `ENOTFOUND`, …) while the
|
|
468
|
+
* abort path shows up as the error's own `name`. */
|
|
469
|
+
function describeFetchFailure(error: unknown, timeoutMs: number): string {
|
|
470
|
+
const err = error as { name?: string; message?: string; cause?: { code?: string } };
|
|
471
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
|
|
472
|
+
return `no response within ${timeoutMs}ms`;
|
|
473
|
+
}
|
|
474
|
+
const code = err?.cause?.code;
|
|
475
|
+
if (typeof code === 'string') return code;
|
|
476
|
+
return err?.message ?? String(error);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function postJson(url: string, body: unknown, timeoutMs: number): Promise<PostOutcome> {
|
|
418
480
|
try {
|
|
419
481
|
const res = await fetch(url, {
|
|
420
482
|
method: 'POST',
|
|
@@ -423,9 +485,9 @@ async function postJson(
|
|
|
423
485
|
signal: AbortSignal.timeout(timeoutMs),
|
|
424
486
|
});
|
|
425
487
|
const json = await res.json().catch(() => undefined);
|
|
426
|
-
return { status: res.status, json };
|
|
427
|
-
} catch {
|
|
428
|
-
return
|
|
488
|
+
return { delivered: true, status: res.status, json };
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return { delivered: false, reason: describeFetchFailure(error, timeoutMs) };
|
|
429
491
|
}
|
|
430
492
|
}
|
|
431
493
|
|
|
@@ -438,8 +500,11 @@ async function relayCommand(
|
|
|
438
500
|
command: Record<string, unknown>,
|
|
439
501
|
timeoutMs: number,
|
|
440
502
|
): Promise<PlayCommandResult> {
|
|
441
|
-
const
|
|
442
|
-
|
|
503
|
+
const target = `${baseUrl(session)}/__editor/command`;
|
|
504
|
+
const result = await postJson(target, command, timeoutMs);
|
|
505
|
+
if (!result.delivered) {
|
|
506
|
+
return { ok: false, error: `Command relay unreachable (${target}): ${result.reason}.` };
|
|
507
|
+
}
|
|
443
508
|
const json = result.json as Record<string, unknown> | undefined;
|
|
444
509
|
// `commandResponseFor` (editor server-utils.ts) SPREADS the browser
|
|
445
510
|
// handler's `data` into the response body at the top level, beside
|
|
@@ -498,7 +563,12 @@ export class HttpPlayTransport implements PlayTransport {
|
|
|
498
563
|
);
|
|
499
564
|
if (body === undefined) return undefined;
|
|
500
565
|
const project = (body as { project?: { path?: string } | null }).project?.path ?? null;
|
|
501
|
-
const info: PlaySessionInfo = {
|
|
566
|
+
const info: PlaySessionInfo = {
|
|
567
|
+
port: s.port,
|
|
568
|
+
project,
|
|
569
|
+
pid: s.pid,
|
|
570
|
+
...(s.kind !== undefined ? { kind: s.kind } : {}),
|
|
571
|
+
};
|
|
502
572
|
return info;
|
|
503
573
|
}),
|
|
504
574
|
);
|
|
@@ -673,7 +743,7 @@ export class HttpPlayTransport implements PlayTransport {
|
|
|
673
743
|
const defaultTransport = new HttpPlayTransport();
|
|
674
744
|
|
|
675
745
|
/** Resolve the transport to use: `ctx['playTransport']` when injected (tests), else the shared real transport. */
|
|
676
|
-
export function getPlayTransport(ctx:
|
|
746
|
+
export function getPlayTransport(ctx: ToolContext): PlayTransport {
|
|
677
747
|
const injected = ctx['playTransport'];
|
|
678
748
|
return (injected as PlayTransport | undefined) ?? defaultTransport;
|
|
679
749
|
}
|
|
@@ -683,7 +753,7 @@ export function getPlayTransport(ctx: OperationContext): PlayTransport {
|
|
|
683
753
|
// + deterministic session resolution
|
|
684
754
|
// ---------------------------------------------------------------------------
|
|
685
755
|
|
|
686
|
-
export const PLAY_RUNTIME_NOT_AVAILABLE_ERROR:
|
|
756
|
+
export const PLAY_RUNTIME_NOT_AVAILABLE_ERROR: ToolErrorDefinition = {
|
|
687
757
|
code: 'EDITOR_NOT_RUNNING',
|
|
688
758
|
summary:
|
|
689
759
|
'No live, responsive editor session is available to host a play runtime (none running, ' +
|
|
@@ -717,12 +787,12 @@ function portOf(url: string): number | undefined {
|
|
|
717
787
|
* > `ctx.projectRoot` match > lowest live port > `EDITOR_NOT_RUNNING`.
|
|
718
788
|
*/
|
|
719
789
|
export async function resolvePlaySession(
|
|
720
|
-
ctx:
|
|
790
|
+
ctx: ToolContext,
|
|
721
791
|
transport: Pick<PlayTransport, 'listSessions' | 'probeSessionUrl'>,
|
|
722
792
|
): Promise<PlaySessionInfo> {
|
|
723
793
|
const editorUrl = ctx.editorUrl;
|
|
724
794
|
const notRunning = (): never => {
|
|
725
|
-
throw new
|
|
795
|
+
throw new ToolError('EDITOR_NOT_RUNNING', 'No editor connected to host a play runtime.', {
|
|
726
796
|
...(editorUrl !== undefined ? { editorUrl } : {}),
|
|
727
797
|
});
|
|
728
798
|
};
|
|
@@ -749,6 +819,7 @@ export async function resolvePlaySession(
|
|
|
749
819
|
} catch {
|
|
750
820
|
return notRunning();
|
|
751
821
|
}
|
|
822
|
+
sessions = sessions.filter((session) => session.kind !== 'e2e');
|
|
752
823
|
if (sessions.length === 0) return notRunning();
|
|
753
824
|
|
|
754
825
|
if (editorUrl !== undefined) {
|