@vgai/sdk 0.5.16 → 0.5.18
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/play/log-format.ts +105 -0
- package/src/play/log-operations.ts +133 -48
- package/src/play/transport.ts +14 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.18",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
33
|
-
"@vgai/engine": "0.5.
|
|
33
|
+
"@vgai/engine": "0.5.18",
|
|
34
34
|
"playwright": "^1.58.2",
|
|
35
35
|
"zod": "^4.3.6"
|
|
36
36
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE on-disk shape of `<project>/logs/play-*.jsonl` — one definition, shared
|
|
3
|
+
* by the writer (`editor-server.ts`'s `/__editor/log-session` +
|
|
4
|
+
* `/__editor/log-entries` handlers) and every reader (`play.log.*` here,
|
|
5
|
+
* `vgai status`'s play-error banner, the project Logs utility).
|
|
6
|
+
*
|
|
7
|
+
* A play log is JSONL with TWO record kinds:
|
|
8
|
+
*
|
|
9
|
+
* 1. THE HEADER — always the FIRST line, written when the file is opened:
|
|
10
|
+
* `{"kind":"session","v":1,"session":…,"project":…,"run":…,"startedAt":…}`.
|
|
11
|
+
* This is where the run's IDENTITY lives, recorded ONCE. The alternative
|
|
12
|
+
* (stamping session/project onto every entry) pays the same constant
|
|
13
|
+
* thousands of times per run for facts that cannot change while the file
|
|
14
|
+
* is open — the file is opened by one session, for one project, at one
|
|
15
|
+
* instant.
|
|
16
|
+
* 2. ENTRIES — every later line: `{t, level, source?, sub?, msg, meta?,
|
|
17
|
+
* tick?, simT?, world?, simSpeed?}`. Only genuinely per-entry facts go
|
|
18
|
+
* here: `tick`/`simT` (the frame the line was written on), `simSpeed`
|
|
19
|
+
* (the live time scale, which an instrument can change mid-run) and
|
|
20
|
+
* `world` (the world the run is presenting, unknown until the roots
|
|
21
|
+
* mount — so the entries written during boot honestly carry none).
|
|
22
|
+
*
|
|
23
|
+
* A reader must SKIP the header when counting or listing entries
|
|
24
|
+
* (`asPlayLogHeader` is the one test), and must tolerate its ABSENCE: logs
|
|
25
|
+
* written before the header existed are still on disk in real projects, and
|
|
26
|
+
* "no header" means "this run's identity was never recorded", never an error.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
30
|
+
|
|
31
|
+
/** `kind` value marking a play log's header line. */
|
|
32
|
+
export const PLAY_LOG_HEADER_KIND = 'session';
|
|
33
|
+
|
|
34
|
+
/** Bumped only when the on-disk record shapes change incompatibly. */
|
|
35
|
+
export const PLAY_LOG_FORMAT_VERSION = 1;
|
|
36
|
+
|
|
37
|
+
/** The run identity a play log carries on its first line. */
|
|
38
|
+
export interface PlayLogHeader {
|
|
39
|
+
readonly kind: typeof PLAY_LOG_HEADER_KIND;
|
|
40
|
+
readonly v: number;
|
|
41
|
+
/** The editor session that opened the file (`processSessionId()`) — the same
|
|
42
|
+
* id `vgai sessions` lists, so a log file names the session that wrote it. */
|
|
43
|
+
readonly session: string;
|
|
44
|
+
/** Absolute project root the session was serving. */
|
|
45
|
+
readonly project: string;
|
|
46
|
+
/** The run's slug (`vgai play --name <text>`), `null` for an unnamed run —
|
|
47
|
+
* the same slug that goes in the filename and the session journal. */
|
|
48
|
+
readonly run: string | null;
|
|
49
|
+
/** Wall-clock ms at which the file was opened. */
|
|
50
|
+
readonly startedAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The header line (newline included) for a run's identity. */
|
|
54
|
+
export function playLogHeaderLine(identity: Omit<PlayLogHeader, 'kind' | 'v'>): string {
|
|
55
|
+
const header: PlayLogHeader = {
|
|
56
|
+
kind: PLAY_LOG_HEADER_KIND,
|
|
57
|
+
v: PLAY_LOG_FORMAT_VERSION,
|
|
58
|
+
...identity,
|
|
59
|
+
};
|
|
60
|
+
return `${JSON.stringify(header)}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The parsed header, or `null` when this record is an ordinary entry (or a
|
|
64
|
+
* header from an unreadable/older shape). Every reader's one header test. */
|
|
65
|
+
export function asPlayLogHeader(record: unknown): PlayLogHeader | null {
|
|
66
|
+
if (typeof record !== 'object' || record === null || Array.isArray(record)) return null;
|
|
67
|
+
const rec = record as Record<string, unknown>;
|
|
68
|
+
if (rec['kind'] !== PLAY_LOG_HEADER_KIND) return null;
|
|
69
|
+
if (typeof rec['session'] !== 'string' || typeof rec['project'] !== 'string') return null;
|
|
70
|
+
return {
|
|
71
|
+
kind: PLAY_LOG_HEADER_KIND,
|
|
72
|
+
v: typeof rec['v'] === 'number' ? rec['v'] : 0,
|
|
73
|
+
session: rec['session'],
|
|
74
|
+
project: rec['project'],
|
|
75
|
+
run: typeof rec['run'] === 'string' ? rec['run'] : null,
|
|
76
|
+
startedAt: typeof rec['startedAt'] === 'number' ? rec['startedAt'] : 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** How much of a log file's start is read to find its header. A header line is
|
|
81
|
+
* a few hundred bytes; anything longer is not one. */
|
|
82
|
+
const HEADER_READ_BYTES = 4096;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The header of the log file at `path`, read WITHOUT loading the file — a
|
|
86
|
+
* listing walks every log in `logs/`, and those files are unbounded (a long
|
|
87
|
+
* run writes megabytes). `null` for an unreadable file or one with no header.
|
|
88
|
+
*/
|
|
89
|
+
export function readPlayLogHeaderFile(path: string): PlayLogHeader | null {
|
|
90
|
+
let fd: number | null = null;
|
|
91
|
+
try {
|
|
92
|
+
fd = openSync(path, 'r');
|
|
93
|
+
const buffer = Buffer.alloc(HEADER_READ_BYTES);
|
|
94
|
+
const bytes = readSync(fd, buffer, 0, HEADER_READ_BYTES, 0);
|
|
95
|
+
const head = buffer.subarray(0, bytes).toString('utf-8');
|
|
96
|
+
const newline = head.indexOf('\n');
|
|
97
|
+
const line = newline === -1 ? head : head.slice(0, newline);
|
|
98
|
+
if (line.trim() === '') return null;
|
|
99
|
+
return asPlayLogHeader(JSON.parse(line));
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
} finally {
|
|
103
|
+
if (fd !== null) closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -12,25 +12,17 @@
|
|
|
12
12
|
* (reusing "the logs/play-*.jsonl reader" per this unit's own brief, taken
|
|
13
13
|
* literally: read the files).
|
|
14
14
|
*
|
|
15
|
-
* IDENTITY FIELDS
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* closest real identity a persisted play run has today.
|
|
27
|
-
* - `project`: `ctx.projectRoot`, always real (these ops require it).
|
|
28
|
-
* - `world`/`simSpeed`: read from each entry's own `meta.world` /
|
|
29
|
-
* `meta.simSpeed`, when the entry happens to carry them (`meta` is
|
|
30
|
-
* already a free-form bag in the real format) — `null` otherwise. This
|
|
31
|
-
* is the honest state of affairs: against TODAY's real files these are
|
|
32
|
-
* always `null` (the current sink never stamps them); a test fixture
|
|
33
|
-
* that DOES embed them in `meta` proves the read-through path for real.
|
|
15
|
+
* IDENTITY FIELDS: every one of them is now WRITTEN, so nothing here
|
|
16
|
+
* synthesizes any of them (`play/log-format.ts` is the format's one
|
|
17
|
+
* definition, shared with the writer):
|
|
18
|
+
* - `session`/`project`/`run`/`startedAt`: the file's HEADER line, written
|
|
19
|
+
* once when the editor server opens the file. Recorded once because they
|
|
20
|
+
* cannot change while it is open.
|
|
21
|
+
* - `world`/`simSpeed`: stamped per ENTRY by `play-mode.ts`'s writers, from
|
|
22
|
+
* the live run (the presented world; `game.loop.timeScale`) — and only
|
|
23
|
+
* when they genuinely know them, so absence is a real answer.
|
|
24
|
+
* A log written before the header existed reads back with `null` identity and
|
|
25
|
+
* no error: those files are still on disk in real projects.
|
|
34
26
|
*
|
|
35
27
|
* `play.log.follow` describes the REAL, existing poll-based subscription
|
|
36
28
|
* for the CURRENTLY ACTIVE play session's log (there is no push/SSE
|
|
@@ -53,6 +45,7 @@ import {
|
|
|
53
45
|
resolveProjectPath,
|
|
54
46
|
} from '../project/shared.js';
|
|
55
47
|
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
48
|
+
import { asPlayLogHeader, type PlayLogHeader, readPlayLogHeaderFile } from './log-format.js';
|
|
56
49
|
import {
|
|
57
50
|
getPlayTransport,
|
|
58
51
|
PLAY_READ_TIMEOUT_MS,
|
|
@@ -75,10 +68,33 @@ const PlayLogDiscoverInput = z
|
|
|
75
68
|
.object({})
|
|
76
69
|
.describe("No input — lists every persisted play-mode log file under the project's logs/ dir.");
|
|
77
70
|
|
|
71
|
+
const PlayLogRunSchema = z.object({
|
|
72
|
+
file: z.string().describe('The log filename this run wrote.'),
|
|
73
|
+
session: z
|
|
74
|
+
.string()
|
|
75
|
+
.nullable()
|
|
76
|
+
.describe(
|
|
77
|
+
'The editor session that wrote it (the id `vgai sessions` lists), from the file header — ' +
|
|
78
|
+
'null for a log written before headers existed.',
|
|
79
|
+
),
|
|
80
|
+
project: z.string().nullable().describe('Absolute project root the session was serving.'),
|
|
81
|
+
run: z
|
|
82
|
+
.string()
|
|
83
|
+
.nullable()
|
|
84
|
+
.describe('The run name slug (`vgai play --name`), null for an unnamed run.'),
|
|
85
|
+
startedAt: z.number().nullable().describe('Wall-clock ms at which the run opened its log.'),
|
|
86
|
+
});
|
|
87
|
+
|
|
78
88
|
const PlayLogDiscoverResult = z.object({
|
|
79
89
|
logFiles: z
|
|
80
90
|
.array(z.string())
|
|
81
91
|
.describe('Filenames (e.g. "play-2026-07-11T10-00-00.jsonl"), oldest first (name-sorted).'),
|
|
92
|
+
runs: z
|
|
93
|
+
.array(PlayLogRunSchema)
|
|
94
|
+
.describe(
|
|
95
|
+
"The same files with each one's recorded identity, read from its header line — this is " +
|
|
96
|
+
"how a caller picks ITS session's log out of the directory instead of guessing by name.",
|
|
97
|
+
),
|
|
82
98
|
});
|
|
83
99
|
|
|
84
100
|
export const playLogDiscover = defineTool({
|
|
@@ -87,7 +103,8 @@ export const playLogDiscover = defineTool({
|
|
|
87
103
|
description:
|
|
88
104
|
'Reads <projectRoot>/logs/ directly off disk and filters play-*.jsonl (the exact naming ' +
|
|
89
105
|
"convention editor-server.ts's log-session handler writes). Works with no editor/play " +
|
|
90
|
-
'session running — these files persist after play stops.'
|
|
106
|
+
'session running — these files persist after play stops. Each file also reports the ' +
|
|
107
|
+
'session/project/run identity from its own header line (null for pre-header logs).',
|
|
91
108
|
input: PlayLogDiscoverInput,
|
|
92
109
|
result: PlayLogDiscoverResult,
|
|
93
110
|
errors: [NO_PROJECT_ROOT_ERROR, PATH_OUTSIDE_PROJECT_ERROR],
|
|
@@ -106,7 +123,17 @@ export const playLogDiscover = defineTool({
|
|
|
106
123
|
names = [];
|
|
107
124
|
}
|
|
108
125
|
const logFiles = names.filter((f) => f.startsWith('play-') && f.endsWith('.jsonl')).sort();
|
|
109
|
-
|
|
126
|
+
const runs = logFiles.map((file) => {
|
|
127
|
+
const header = readPlayLogHeaderFile(join(logsDir, file));
|
|
128
|
+
return {
|
|
129
|
+
file,
|
|
130
|
+
session: header?.session ?? null,
|
|
131
|
+
project: header?.project ?? null,
|
|
132
|
+
run: header?.run ?? null,
|
|
133
|
+
startedAt: header?.startedAt ?? null,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
return { logFiles, runs };
|
|
110
137
|
},
|
|
111
138
|
});
|
|
112
139
|
|
|
@@ -121,21 +148,33 @@ const PlayLogEntrySchema = z.object({
|
|
|
121
148
|
sub: z.string().nullable(),
|
|
122
149
|
msg: z.string(),
|
|
123
150
|
meta: z.record(z.string(), z.unknown()).nullable(),
|
|
124
|
-
|
|
151
|
+
tick: z
|
|
152
|
+
.number()
|
|
153
|
+
.nullable()
|
|
154
|
+
.describe('Engine fixed-step counter at write time, when the writer could read it.'),
|
|
155
|
+
simT: z.number().nullable().describe('Accumulated sim-seconds at write time — same source.'),
|
|
156
|
+
session: z
|
|
157
|
+
.string()
|
|
158
|
+
.nullable()
|
|
159
|
+
.describe(
|
|
160
|
+
"The editor session that wrote this log, from the file's header line — null for a log " +
|
|
161
|
+
'written before headers existed.',
|
|
162
|
+
),
|
|
125
163
|
project: z.string().describe('Absolute project root this log belongs to.'),
|
|
126
164
|
world: z
|
|
127
165
|
.string()
|
|
128
166
|
.nullable()
|
|
129
167
|
.describe(
|
|
130
|
-
'
|
|
131
|
-
|
|
168
|
+
'The world the run was presenting when this entry was written, as the play-mode writer ' +
|
|
169
|
+
'stamped it — null when it had no unambiguous world (boot-time entries, multi-root runs ' +
|
|
170
|
+
'presenting none).',
|
|
132
171
|
),
|
|
133
172
|
simSpeed: z
|
|
134
173
|
.number()
|
|
135
174
|
.nullable()
|
|
136
175
|
.describe(
|
|
137
|
-
'
|
|
138
|
-
|
|
176
|
+
"The loop's live time scale when this entry was written (1 real time, 0 paused) — null " +
|
|
177
|
+
'when no game was running yet.',
|
|
139
178
|
),
|
|
140
179
|
});
|
|
141
180
|
|
|
@@ -157,24 +196,25 @@ const PlayLogReadInput = z.object({
|
|
|
157
196
|
|
|
158
197
|
const PlayLogReadResult = z.object({
|
|
159
198
|
entries: z.array(PlayLogEntrySchema),
|
|
160
|
-
totalEntries: z
|
|
199
|
+
totalEntries: z
|
|
200
|
+
.number()
|
|
201
|
+
.describe('Total entries in the file, regardless of offset/limit (the header is not one).'),
|
|
202
|
+
run: PlayLogRunSchema.describe("This log's own recorded identity, from its header line."),
|
|
161
203
|
});
|
|
162
204
|
|
|
163
205
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
164
206
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
165
207
|
}
|
|
166
208
|
|
|
167
|
-
/** Parse one raw JSONL line
|
|
209
|
+
/** Parse one raw JSONL entry line, carrying the file-level session identity
|
|
210
|
+
* from the header onto it. Every field is read from what the writer actually
|
|
211
|
+
* wrote; nothing is derived from the filename or invented. */
|
|
168
212
|
function parseLogLine(
|
|
169
|
-
|
|
170
|
-
session: string,
|
|
213
|
+
rec: Record<string, unknown>,
|
|
214
|
+
session: string | null,
|
|
171
215
|
project: string,
|
|
172
216
|
): z.infer<typeof PlayLogEntrySchema> {
|
|
173
|
-
const parsed: unknown = JSON.parse(line);
|
|
174
|
-
const rec = isRecord(parsed) ? parsed : {};
|
|
175
217
|
const meta = isRecord(rec['meta']) ? (rec['meta'] as Record<string, unknown>) : null;
|
|
176
|
-
const worldRaw = meta?.['world'];
|
|
177
|
-
const simSpeedRaw = meta?.['simSpeed'];
|
|
178
218
|
return {
|
|
179
219
|
t: typeof rec['t'] === 'number' ? rec['t'] : 0,
|
|
180
220
|
level: typeof rec['level'] === 'string' ? rec['level'] : 'log',
|
|
@@ -182,10 +222,12 @@ function parseLogLine(
|
|
|
182
222
|
sub: typeof rec['sub'] === 'string' ? rec['sub'] : null,
|
|
183
223
|
msg: typeof rec['msg'] === 'string' ? rec['msg'] : '',
|
|
184
224
|
meta,
|
|
225
|
+
tick: typeof rec['tick'] === 'number' ? rec['tick'] : null,
|
|
226
|
+
simT: typeof rec['simT'] === 'number' ? rec['simT'] : null,
|
|
185
227
|
session,
|
|
186
228
|
project,
|
|
187
|
-
world: typeof
|
|
188
|
-
simSpeed: typeof
|
|
229
|
+
world: typeof rec['world'] === 'string' ? rec['world'] : null,
|
|
230
|
+
simSpeed: typeof rec['simSpeed'] === 'number' ? rec['simSpeed'] : null,
|
|
189
231
|
};
|
|
190
232
|
}
|
|
191
233
|
|
|
@@ -193,10 +235,10 @@ export const playLogRead = defineTool({
|
|
|
193
235
|
name: 'play.log.read',
|
|
194
236
|
summary: 'Read entries from a persisted play-mode log file.',
|
|
195
237
|
description:
|
|
196
|
-
|
|
197
|
-
'
|
|
198
|
-
|
|
199
|
-
'
|
|
238
|
+
"Reads <projectRoot>/logs/<file> directly off disk, parsing each JSONL line. The file's " +
|
|
239
|
+
'header line supplies the run identity (session/project/run/startedAt, reported as `run` and ' +
|
|
240
|
+
'carried onto every entry); world/simSpeed/tick/simT come from each entry as the play-mode ' +
|
|
241
|
+
'writer stamped them. A pre-header log reads back with null identity, never an error.',
|
|
200
242
|
input: PlayLogReadInput,
|
|
201
243
|
result: PlayLogReadResult,
|
|
202
244
|
errors: [NO_PROJECT_ROOT_ERROR, PATH_OUTSIDE_PROJECT_ERROR, FILE_NOT_FOUND_ERROR],
|
|
@@ -209,12 +251,33 @@ export const playLogRead = defineTool({
|
|
|
209
251
|
const projectRoot = requireProjectRoot(ctx);
|
|
210
252
|
const absPath = resolveProjectPath(projectRoot, join('logs', input.file));
|
|
211
253
|
const { raw } = readFileWithHash(absPath);
|
|
212
|
-
const
|
|
254
|
+
const records = raw
|
|
255
|
+
.split('\n')
|
|
256
|
+
.filter((l) => l.trim().length > 0)
|
|
257
|
+
.map((line): unknown => JSON.parse(line));
|
|
258
|
+
// The header is the file's identity, not one of its entries: it is peeled
|
|
259
|
+
// off before offset/limit and before totalEntries, so a caller paging
|
|
260
|
+
// through a log never has to know it is there.
|
|
261
|
+
const header: PlayLogHeader | null = records.length > 0 ? asPlayLogHeader(records[0]) : null;
|
|
262
|
+
const entryRecords = header === null ? records : records.slice(1);
|
|
213
263
|
const offset = input.offset ?? 0;
|
|
214
|
-
const limit = input.limit ??
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
264
|
+
const limit = input.limit ?? entryRecords.length;
|
|
265
|
+
const entries = entryRecords
|
|
266
|
+
.slice(offset, offset + limit)
|
|
267
|
+
.map((record) =>
|
|
268
|
+
parseLogLine(isRecord(record) ? record : {}, header?.session ?? null, projectRoot),
|
|
269
|
+
);
|
|
270
|
+
return {
|
|
271
|
+
entries,
|
|
272
|
+
totalEntries: entryRecords.length,
|
|
273
|
+
run: {
|
|
274
|
+
file: input.file,
|
|
275
|
+
session: header?.session ?? null,
|
|
276
|
+
project: header?.project ?? null,
|
|
277
|
+
run: header?.run ?? null,
|
|
278
|
+
startedAt: header?.startedAt ?? null,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
218
281
|
},
|
|
219
282
|
});
|
|
220
283
|
|
|
@@ -235,7 +298,17 @@ const PlayLogFollowResult = z.object({
|
|
|
235
298
|
.describe('POST {action:"start"|"end"} — already bracketing the active play session.'),
|
|
236
299
|
bufferedLogCount: z
|
|
237
300
|
.number()
|
|
238
|
-
.describe('Live count of entries buffered for the active play session.'),
|
|
301
|
+
.describe('Live count of entries buffered for the active play session (header excluded).'),
|
|
302
|
+
file: z
|
|
303
|
+
.string()
|
|
304
|
+
.nullable()
|
|
305
|
+
.describe("The active run's log filename under logs/ — what play.log.read should be given."),
|
|
306
|
+
session: z
|
|
307
|
+
.string()
|
|
308
|
+
.nullable()
|
|
309
|
+
.describe("The editor session that opened it, from the file's header line."),
|
|
310
|
+
project: z.string().nullable().describe('Absolute project root that session is serving.'),
|
|
311
|
+
run: z.string().nullable().describe('The run name slug, null for an unnamed run.'),
|
|
239
312
|
});
|
|
240
313
|
|
|
241
314
|
export const playLogFollow = defineTool({
|
|
@@ -244,7 +317,8 @@ export const playLogFollow = defineTool({
|
|
|
244
317
|
description:
|
|
245
318
|
'Metadata only (no push/SSE for log lines exists today): reports the real ' +
|
|
246
319
|
'GET/POST /__editor/log-entries + /__editor/log-session endpoints for the CURRENTLY active ' +
|
|
247
|
-
'play session, plus a live buffered-entry count
|
|
320
|
+
'play session, plus a live buffered-entry count and the run identity the active log file ' +
|
|
321
|
+
'recorded in its header — same contract as editor.console.subscribe.',
|
|
248
322
|
input: PlayLogFollowInput,
|
|
249
323
|
result: PlayLogFollowResult,
|
|
250
324
|
errors: [PLAY_RUNTIME_NOT_AVAILABLE_ERROR, PLAY_LOG_FOLLOW_UNAVAILABLE_ERROR],
|
|
@@ -268,7 +342,18 @@ export const playLogFollow = defineTool({
|
|
|
268
342
|
{},
|
|
269
343
|
);
|
|
270
344
|
}
|
|
271
|
-
|
|
345
|
+
// A transport that predates the header (or an older editor answering it)
|
|
346
|
+
// reports the endpoints and the count and says `null` for the identity —
|
|
347
|
+
// absence, never a name inferred from somewhere else.
|
|
348
|
+
return {
|
|
349
|
+
logEntriesUrl: metadata.logEntriesUrl,
|
|
350
|
+
logSessionUrl: metadata.logSessionUrl,
|
|
351
|
+
bufferedLogCount: metadata.bufferedLogCount,
|
|
352
|
+
file: metadata.file ?? null,
|
|
353
|
+
session: metadata.session ?? null,
|
|
354
|
+
project: metadata.project ?? null,
|
|
355
|
+
run: metadata.run ?? null,
|
|
356
|
+
};
|
|
272
357
|
},
|
|
273
358
|
});
|
|
274
359
|
|
package/src/play/transport.ts
CHANGED
|
@@ -79,6 +79,7 @@ import { z } from 'zod';
|
|
|
79
79
|
import { ToolError } from '../errors.js';
|
|
80
80
|
import type { ToolErrorDefinition } from '../registry.js';
|
|
81
81
|
import type { ToolContext } from '../types.js';
|
|
82
|
+
import { asPlayLogHeader } from './log-format.js';
|
|
82
83
|
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
84
85
|
// Named timeouts
|
|
@@ -239,6 +240,13 @@ export interface PlayLogFollowMetadata {
|
|
|
239
240
|
logEntriesUrl: string;
|
|
240
241
|
logSessionUrl: string;
|
|
241
242
|
bufferedLogCount: number;
|
|
243
|
+
/** The active log's filename and the identity its header line recorded
|
|
244
|
+
* (`@vgai/sdk`'s `play/log-format.ts`). Optional on the seam so a fake
|
|
245
|
+
* transport may omit what it has no wire for; the op reports `null`. */
|
|
246
|
+
file?: string | null;
|
|
247
|
+
session?: string | null;
|
|
248
|
+
project?: string | null;
|
|
249
|
+
run?: string | null;
|
|
242
250
|
}
|
|
243
251
|
|
|
244
252
|
export type InputInjectionKind = 'axis' | 'vector2' | 'pointerDelta' | 'pointerPosition';
|
|
@@ -633,13 +641,18 @@ export class HttpPlayTransport implements PlayTransport {
|
|
|
633
641
|
timeoutMs: number,
|
|
634
642
|
): Promise<PlayLogFollowMetadata | undefined> {
|
|
635
643
|
const body = (await fetchJson(`${baseUrl(session)}/__editor/log-entries`, timeoutMs)) as
|
|
636
|
-
| { entries?: unknown[] }
|
|
644
|
+
| { entries?: unknown[]; header?: unknown; file?: unknown }
|
|
637
645
|
| undefined;
|
|
638
646
|
if (!body) return undefined;
|
|
647
|
+
const header = asPlayLogHeader(body.header);
|
|
639
648
|
return {
|
|
640
649
|
logEntriesUrl: `${baseUrl(session)}/__editor/log-entries`,
|
|
641
650
|
logSessionUrl: `${baseUrl(session)}/__editor/log-session`,
|
|
642
651
|
bufferedLogCount: Array.isArray(body.entries) ? body.entries.length : 0,
|
|
652
|
+
file: typeof body.file === 'string' ? body.file : null,
|
|
653
|
+
session: header?.session ?? null,
|
|
654
|
+
project: header?.project ?? null,
|
|
655
|
+
run: header?.run ?? null,
|
|
643
656
|
};
|
|
644
657
|
}
|
|
645
658
|
|