@vgai/sdk 0.5.48 → 0.5.50

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 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.48",
5
+ "version": "0.5.50",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -21,17 +21,19 @@
21
21
  "./build-discipline": "./src/project/build-discipline.ts",
22
22
  "./generations": "./src/generations.ts",
23
23
  "./mcp-stdio": "./src/mcp/mcp-stdio-server.ts",
24
+ "./output-roots": "./src/project/output-roots.ts",
24
25
  "./project-tool-catalog": "./src/project-tool-catalog.ts",
25
26
  "./project-inspection-node": "./src/project/inspection-node.ts",
26
27
  "./registry": "./src/registry.ts",
27
28
  "./run-name": "./src/project/run-name.ts",
28
29
  "./session-journal": "./src/project/session-journal.ts",
29
30
  "./tab-census": "./src/project/tab-census.ts",
30
- "./tools": "./src/tools.ts"
31
+ "./tools": "./src/tools.ts",
32
+ "./provider-execution": "./src/provider-execution.ts"
31
33
  },
32
34
  "dependencies": {
33
35
  "@modelcontextprotocol/sdk": "^1.30.0",
34
- "@vgai/engine": "0.5.48",
36
+ "@vgai/engine": "0.5.50",
35
37
  "playwright": "^1.58.2",
36
38
  "zod": "^4.3.6"
37
39
  }
package/src/account.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- // Which account backend this session is signed into: the zero-charge in-process mock control plane, or the live VGAI account service. NOT a generation switch — generation execution routes (mock/managed/byok) are chosen per call; see preferredRoute for the UI default.
3
+ // Account identity and billing preference are host configuration, never generation-call inputs.
4
4
  export const AccountBackendSchema = z.enum(['mock', 'live']);
5
5
  export const AccountPlanSchema = z.object({
6
6
  id: z.string().min(1),
@@ -123,7 +123,7 @@ export const AccountSnapshotSchema = z.discriminatedUnion('authenticated', [
123
123
  ]);
124
124
  /** Product-facing routes. Provider tools may translate BYOK to their native
125
125
  * transport name (`direct`) internally. */
126
- export const GenerationExecutionRouteSchema = z.enum(['mock', 'managed', 'byok']);
126
+ export const GenerationExecutionRouteSchema = z.enum(['auto', 'mock', 'managed', 'byok']);
127
127
  export const ProviderCredentialIdSchema = z.enum(['fal', 'tripo', 'worldlabs', 'openrouter']);
128
128
  export const CodingInferenceSettingsSchema = z.object({
129
129
  enabled: z.boolean(),
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { join as joinPath } from 'node:path';
27
- import { type Browser, chromium } from 'playwright';
27
+ import type { Browser } from 'playwright';
28
28
  import {
29
29
  DEFAULT_ENGINE_ROOT,
30
30
  findFreeRenderPort,
@@ -138,7 +138,7 @@ export async function runPerfCapture(request: PerfRunRequest): Promise<PerfRunRe
138
138
  const server = await launchEntryServer(entry, port, engineRoot, undefined);
139
139
  let browser: Browser | undefined;
140
140
  try {
141
- browser = await chromium.launch({
141
+ browser = await (await import('playwright')).chromium.launch({
142
142
  headless: true,
143
143
  args: ['--use-gl=angle', '--use-angle=swiftshader'],
144
144
  // Cancellation/exit is owned by the caller, never Playwright's own
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The project directories a tool may commit generated bytes into, and the one
3
+ * normalizer every writer validates a path with.
4
+ *
5
+ * A project has two of them, and the difference is what SHIPS:
6
+ *
7
+ * - `public/` — game assets. Exported, served at a root-relative URL,
8
+ * loaded by the game's own source.
9
+ * - `references/` — reference material: moodboards, style plates, generated
10
+ * stills and clips an author or an agent looks at while
11
+ * building. Ordinary files, indexed by the editor's Content
12
+ * panel like any other media, and NOT exported — the
13
+ * project's own build copies `publicDir` and nothing else.
14
+ *
15
+ * This lives in the SDK rather than beside one writer because the rule has more
16
+ * than one enforcer: the editor host's atomic project-output writer, and every
17
+ * registered provider boundary that takes an `outputDirectory` from a caller.
18
+ * Two copies of "must be under public/" is how a reference output becomes
19
+ * writable through one door and refused at the next.
20
+ */
21
+
22
+ /** The writable output roots, in the order a chooser should offer them. */
23
+ export const PROJECT_OUTPUT_ROOTS = ['public', 'references'] as const;
24
+
25
+ export type ProjectOutputRoot = (typeof PROJECT_OUTPUT_ROOTS)[number];
26
+
27
+ /** Human-readable list for an error message: `public/` or `references/`. */
28
+ function rootList(): string {
29
+ return PROJECT_OUTPUT_ROOTS.map((root) => `${root}/`).join(' or ');
30
+ }
31
+
32
+ /** POSIX-normalized, `./`-stripped, trailing-slash-stripped. */
33
+ function normalizeSlashes(path: string): string {
34
+ const slashed = path.replaceAll('\\', '/');
35
+ const segments: string[] = [];
36
+ for (const segment of slashed.split('/')) {
37
+ if (segment === '' || segment === '.') continue;
38
+ segments.push(segment);
39
+ }
40
+ return segments.join('/');
41
+ }
42
+
43
+ /** The root `path` sits under, or `undefined` when it sits under none. */
44
+ export function projectOutputRootOf(path: string): ProjectOutputRoot | undefined {
45
+ const normalized = normalizeSlashes(path);
46
+ return PROJECT_OUTPUT_ROOTS.find(
47
+ (root) => normalized === root || normalized.startsWith(`${root}/`),
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Validate and normalize a project-relative output path.
53
+ *
54
+ * Throws — with a message naming BOTH roots, because the common mistake is
55
+ * knowing about one of them — for an absolute path, an escape, a bare root
56
+ * with no file under it, or anything outside the roots above. `subject` names
57
+ * what is being validated in that message (`Generated output path`, `Fal
58
+ * outputDirectory`, …).
59
+ */
60
+ export function normalizeProjectOutputPath(
61
+ path: string,
62
+ subject = 'Generated output path',
63
+ ): string {
64
+ const normalized = normalizeSlashes(path);
65
+ const escapes = normalized.split('/').includes('..');
66
+ const root = projectOutputRootOf(normalized);
67
+ if (path.startsWith('/') || escapes || root === undefined || normalized === root) {
68
+ throw new Error(
69
+ `${subject} ${JSON.stringify(path)} is invalid; it must be a project-relative path under ${rootList()}.`,
70
+ );
71
+ }
72
+ return normalized;
73
+ }
@@ -237,6 +237,12 @@ export type SessionJournalEvent =
237
237
  }
238
238
  /** The tab said its command listener PICKED THE COMMAND UP. */
239
239
  | { readonly kind: 'command-receipt'; readonly requestId8: string }
240
+ /**
241
+ * The last tab's connection went away while UNRECEIPTED commands were still
242
+ * queued, and this many were swept. Receipted ones are deliberately not
243
+ * counted here — they keep their own work budget.
244
+ */
245
+ | { readonly kind: 'command-swept-on-last-tab-gone'; readonly settled: number }
240
246
  /** The command settled — by the tab's own answer or by the relay refusing. */
241
247
  | {
242
248
  readonly kind: 'command-result';
@@ -640,6 +646,8 @@ export function formatJournalLine(line: SessionJournalLine): string {
640
646
  return `journal: ${at} command-relayed ${line.command} ${line.requestId8} -> tab ${line.tabId8 ?? 'none'}`;
641
647
  case 'command-receipt':
642
648
  return `journal: ${at} command-receipt ${line.requestId8}`;
649
+ case 'command-swept-on-last-tab-gone':
650
+ return `journal: ${at} command-swept-on-last-tab-gone ${line.settled} unreceipted`;
643
651
  case 'command-result':
644
652
  return `journal: ${at} command-result ${line.requestId8} ${line.ok ? 'ok' : `FAILED ${line.error ?? ''}`}`;
645
653
  case 'command-held':
@@ -0,0 +1,70 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { GenerationJobDraft } from './generations.js';
3
+
4
+ export type ProviderExecutionMode = 'direct' | 'managed' | 'mock';
5
+ export type ProviderModeResolver = (
6
+ provider: string,
7
+ requestId?: string,
8
+ ) => Promise<ProviderExecutionMode>;
9
+
10
+ const credentialNames: Record<string, string> = {
11
+ fal: 'FAL_KEY',
12
+ tripo: 'TRIPO_API_KEY',
13
+ worldlabs: 'WORLDLABS_API_KEY',
14
+ openrouter: 'OPENROUTER_API_KEY',
15
+ };
16
+
17
+ // Project SSR and installed packages can load separate module instances.
18
+ // The host's async context must still be shared, without sharing requests.
19
+ const key = Symbol.for('vgai.provider-execution-context');
20
+ const globals = globalThis as typeof globalThis & {
21
+ [key]?: AsyncLocalStorage<{
22
+ resolve: ProviderModeResolver;
23
+ record?: ((job: GenerationJobDraft) => Promise<void>) | undefined;
24
+ }>;
25
+ };
26
+ const context = globals[key] ?? new AsyncLocalStorage();
27
+ globals[key] = context;
28
+
29
+ /** Trusted host configuration. Provider choice and billing never enter tool input. */
30
+ export function withProviderExecution<T>(
31
+ resolve: ProviderModeResolver,
32
+ work: () => Promise<T>,
33
+ record?: (job: GenerationJobDraft) => Promise<void>,
34
+ ) {
35
+ const pending = new Map<string, Promise<ProviderExecutionMode>>();
36
+ return context.run(
37
+ {
38
+ record,
39
+ resolve: (provider, requestId) => {
40
+ const key = JSON.stringify([provider, requestId]);
41
+ let result = pending.get(key);
42
+ if (!result) {
43
+ result = resolve(provider, requestId);
44
+ pending.set(key, result);
45
+ }
46
+ return result;
47
+ },
48
+ },
49
+ work,
50
+ );
51
+ }
52
+
53
+ /** Native provider clients use the same project ledger as registered tools. */
54
+ export async function recordProviderGeneration(job: GenerationJobDraft): Promise<void> {
55
+ await context.getStore()?.record?.(job);
56
+ }
57
+
58
+ /** Library-side connection resolution; callers supply only native provider input. */
59
+ export async function resolveProviderMode(
60
+ provider: string,
61
+ requestId?: string,
62
+ ): Promise<ProviderExecutionMode> {
63
+ const resolve = context.getStore()?.resolve;
64
+ if (resolve) return resolve(provider, requestId);
65
+ const name = credentialNames[provider];
66
+ if (!name) throw new Error(`Unknown generation provider ${JSON.stringify(provider)}.`);
67
+ if (process.env[name]?.trim()) return 'direct';
68
+ if (process.env['VGAI_GENERATION_GATEWAY'] && process.env['VGAI_ACCESS_TOKEN']) return 'managed';
69
+ throw new Error(`Connect ${provider} or sign in to VGAI in Account before generating.`);
70
+ }
@@ -34,7 +34,6 @@ import { tmpdir } from 'node:os';
34
34
  import { dirname, join, resolve as resolvePath } from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
  import type { Browser, Page } from 'playwright';
37
- import { chromium } from 'playwright';
38
37
  import { checkFfmpegCapability, type FfmpegCapabilityError } from './capabilities/ffmpeg';
39
38
 
40
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -1539,7 +1538,7 @@ export async function openRenderCinematicPage(
1539
1538
  );
1540
1539
  let browser: Browser | undefined;
1541
1540
  try {
1542
- browser = await chromium.launch({
1541
+ browser = await (await import('playwright')).chromium.launch({
1543
1542
  headless: true,
1544
1543
  args: ['--use-gl=angle', '--use-angle=swiftshader'],
1545
1544
  // B7 review fold-in (SIGINT race): Playwright's DEFAULT is to install
@@ -1836,7 +1835,7 @@ export async function renderCinematic(
1836
1835
  signal?.addEventListener('abort', onAbort, { once: true });
1837
1836
  try {
1838
1837
  onProgress?.({ phase: 'browser-launch' });
1839
- browser = await chromium.launch({
1838
+ browser = await (await import('playwright')).chromium.launch({
1840
1839
  headless: true,
1841
1840
  args: ['--use-gl=angle', '--use-angle=swiftshader'],
1842
1841
  // B7 review fold-in (SIGINT race, #10): without this, Playwright