@vibevibes/mcp 0.5.0 → 0.6.0
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/dist/server.js +1 -87
- package/hooks/logic.js +2 -5
- package/package.json +1 -1
- package/dist/protocol.d.ts +0 -85
- package/dist/protocol.js +0 -240
- package/dist/tick-engine.d.ts +0 -81
- package/dist/tick-engine.js +0 -151
package/dist/server.js
CHANGED
|
@@ -15,8 +15,6 @@ import { ZodError } from "zod";
|
|
|
15
15
|
import { EventEmitter } from "events";
|
|
16
16
|
import { bundleForServer, bundleForClient, evalServerBundle, validateClientBundle } from "./bundler.js";
|
|
17
17
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
18
|
-
import { TickEngine } from "./tick-engine.js";
|
|
19
|
-
import { isProtocolExperience, loadProtocolManifest, createProtocolModule, SubprocessExecutor } from "./protocol.js";
|
|
20
18
|
function formatZodError(err, toolName, tool) {
|
|
21
19
|
const issues = err.issues.map((issue) => {
|
|
22
20
|
const path = issue.path.length > 0 ? `'${issue.path.join(".")}'` : "input";
|
|
@@ -222,7 +220,6 @@ const HISTORY_MAX_LIMIT = 200;
|
|
|
222
220
|
const DEFAULT_STREAM_RATE_LIMIT = 60;
|
|
223
221
|
const STREAM_RATE_WINDOW_MS = 1000;
|
|
224
222
|
const EVENT_BATCH_DEBOUNCE_MS = 50;
|
|
225
|
-
const DEFAULT_TICK_RATE_MS = 50;
|
|
226
223
|
const MAX_BATCH_CALLS = 10;
|
|
227
224
|
const LONG_POLL_MAX_TIMEOUT_MS = 55000;
|
|
228
225
|
const AGENT_CONTEXT_MAX_TIMEOUT_MS = 10000;
|
|
@@ -253,7 +250,6 @@ function defaultObserve(state, _event, _actorId) {
|
|
|
253
250
|
let PORT = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
|
|
254
251
|
let publicUrl = null;
|
|
255
252
|
let room;
|
|
256
|
-
let tickEngine = null;
|
|
257
253
|
let _actorCounter = 0;
|
|
258
254
|
const roomEvents = new EventEmitter();
|
|
259
255
|
roomEvents.setMaxListeners(ROOM_EVENTS_MAX_LISTENERS);
|
|
@@ -345,9 +341,6 @@ function experienceNotLoadedError() {
|
|
|
345
341
|
}
|
|
346
342
|
// ── Experience discovery & loading ──────────────────────────
|
|
347
343
|
function discoverEntryPath() {
|
|
348
|
-
const manifestPath = path.join(PROJECT_ROOT, "manifest.json");
|
|
349
|
-
if (fs.existsSync(manifestPath))
|
|
350
|
-
return manifestPath;
|
|
351
344
|
const tsxPath = path.join(PROJECT_ROOT, "src", "index.tsx");
|
|
352
345
|
if (fs.existsSync(tsxPath))
|
|
353
346
|
return tsxPath;
|
|
@@ -355,15 +348,10 @@ function discoverEntryPath() {
|
|
|
355
348
|
if (fs.existsSync(rootTsx))
|
|
356
349
|
return rootTsx;
|
|
357
350
|
throw new Error(`No experience found in ${PROJECT_ROOT}. ` +
|
|
358
|
-
`Create
|
|
351
|
+
`Create src/index.tsx (TypeScript).`);
|
|
359
352
|
}
|
|
360
|
-
const protocolExecutors = new Map();
|
|
361
353
|
async function loadExperience() {
|
|
362
354
|
const entryPath = discoverEntryPath();
|
|
363
|
-
if (isProtocolExperience(entryPath)) {
|
|
364
|
-
await loadProtocolExperience(entryPath);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
355
|
const [sCode, cCode] = await Promise.all([
|
|
368
356
|
bundleForServer(entryPath),
|
|
369
357
|
bundleForClient(entryPath),
|
|
@@ -385,48 +373,6 @@ async function loadExperience() {
|
|
|
385
373
|
};
|
|
386
374
|
experienceError = null;
|
|
387
375
|
}
|
|
388
|
-
async function loadProtocolExperience(manifestPath) {
|
|
389
|
-
const manifestDir = path.dirname(manifestPath);
|
|
390
|
-
const manifest = loadProtocolManifest(manifestPath);
|
|
391
|
-
const existing = protocolExecutors.get(manifest.id);
|
|
392
|
-
if (existing)
|
|
393
|
-
existing.stop();
|
|
394
|
-
const executor = new SubprocessExecutor(manifest.toolProcess.command, manifest.toolProcess.args || [], manifestDir);
|
|
395
|
-
executor.start();
|
|
396
|
-
protocolExecutors.set(manifest.id, executor);
|
|
397
|
-
try {
|
|
398
|
-
await executor.send("init", { experienceId: manifest.id }, 5000);
|
|
399
|
-
}
|
|
400
|
-
catch { }
|
|
401
|
-
const mod = createProtocolModule(manifest, executor, manifestDir);
|
|
402
|
-
loadedExperience = {
|
|
403
|
-
module: mod,
|
|
404
|
-
clientBundle: "",
|
|
405
|
-
serverCode: JSON.stringify(manifest),
|
|
406
|
-
loadedAt: Date.now(),
|
|
407
|
-
sourcePath: manifestPath,
|
|
408
|
-
};
|
|
409
|
-
experienceError = null;
|
|
410
|
-
console.log(`[protocol] Loaded ${manifest.title} (${manifest.id}) — ${manifest.tools.length} tools`);
|
|
411
|
-
}
|
|
412
|
-
// ── Tick Engine lifecycle ──────────────────────────────────
|
|
413
|
-
function maybeStartTickEngine() {
|
|
414
|
-
if (!loadedExperience)
|
|
415
|
-
return;
|
|
416
|
-
const manifest = loadedExperience.module?.manifest;
|
|
417
|
-
if (manifest?.netcode !== "tick")
|
|
418
|
-
return;
|
|
419
|
-
stopTickEngine();
|
|
420
|
-
const tickRateMs = manifest.tickRateMs || DEFAULT_TICK_RATE_MS;
|
|
421
|
-
tickEngine = new TickEngine(room, loadedExperience, roomEvents, tickRateMs);
|
|
422
|
-
tickEngine.start();
|
|
423
|
-
}
|
|
424
|
-
function stopTickEngine() {
|
|
425
|
-
if (tickEngine) {
|
|
426
|
-
tickEngine.stop();
|
|
427
|
-
tickEngine = null;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
376
|
// ── Express app ────────────────────────────────────────────
|
|
431
377
|
const app = express();
|
|
432
378
|
app.use(express.json({ limit: JSON_BODY_LIMIT }));
|
|
@@ -1186,8 +1132,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1186
1132
|
return room.events.filter(e => {
|
|
1187
1133
|
if (requestingOwner && e.owner === requestingOwner)
|
|
1188
1134
|
return false;
|
|
1189
|
-
if (e.actorId === "_tick-engine" || e.owner === "_system")
|
|
1190
|
-
return false;
|
|
1191
1135
|
return e.ts > since;
|
|
1192
1136
|
}).sort((a, b) => a.ts - b.ts);
|
|
1193
1137
|
};
|
|
@@ -1239,7 +1183,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1239
1183
|
lastError,
|
|
1240
1184
|
browserErrors: recentBrowserErrors.length > 0 ? recentBrowserErrors : undefined,
|
|
1241
1185
|
participants: room.participantList(),
|
|
1242
|
-
tickEngine: tickEngine ? { enabled: true, tickCount: tickEngine.getStatus().tickCount } : undefined,
|
|
1243
1186
|
eventCursor,
|
|
1244
1187
|
};
|
|
1245
1188
|
};
|
|
@@ -1383,10 +1326,8 @@ app.post("/reset", (_req, res) => {
|
|
|
1383
1326
|
// ── Sync (re-bundle) ──────────────────────────────────────
|
|
1384
1327
|
app.post("/sync", async (_req, res) => {
|
|
1385
1328
|
try {
|
|
1386
|
-
stopTickEngine();
|
|
1387
1329
|
await loadExperience();
|
|
1388
1330
|
room.broadcastToAll({ type: "experience_updated" });
|
|
1389
|
-
maybeStartTickEngine();
|
|
1390
1331
|
const mod = getModule();
|
|
1391
1332
|
res.json({ synced: true, title: mod?.manifest?.title });
|
|
1392
1333
|
}
|
|
@@ -1411,14 +1352,6 @@ app.get("/experiences", async (_req, res) => {
|
|
|
1411
1352
|
tags: mod.manifest.tags,
|
|
1412
1353
|
}]);
|
|
1413
1354
|
});
|
|
1414
|
-
// ── Tick status ────────────────────────────────────────────
|
|
1415
|
-
app.get("/tick-status", (_req, res) => {
|
|
1416
|
-
if (!tickEngine) {
|
|
1417
|
-
res.json({ enabled: false, tickRateMs: 0, tickCount: 0, behaviorsTotal: 0, behaviorsEnabled: 0, lastTick: null });
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
res.json(tickEngine.getStatus());
|
|
1421
|
-
});
|
|
1422
1355
|
// ── MCP config ─────────────────────────────────────────────
|
|
1423
1356
|
app.get("/mcp-config", (_req, res) => {
|
|
1424
1357
|
const serverUrl = getBaseUrl();
|
|
@@ -1436,21 +1369,6 @@ app.get("/mcp-config", (_req, res) => {
|
|
|
1436
1369
|
],
|
|
1437
1370
|
});
|
|
1438
1371
|
});
|
|
1439
|
-
// ── Protocol experience HTML canvas ───────────────────────
|
|
1440
|
-
app.get("/canvas", async (_req, res) => {
|
|
1441
|
-
if (!loadedExperience) {
|
|
1442
|
-
res.status(500).json({ error: experienceNotLoadedError() });
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
const canvasPath = loadedExperience.module?._canvasPath;
|
|
1446
|
-
if (!canvasPath) {
|
|
1447
|
-
res.status(404).json({ error: "This experience has no HTML canvas" });
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
res.setHeader("Content-Type", "text/html");
|
|
1451
|
-
setNoCacheHeaders(res);
|
|
1452
|
-
res.sendFile(canvasPath);
|
|
1453
|
-
});
|
|
1454
1372
|
// ── Catch-all: serve viewer ─────────────────────────────────
|
|
1455
1373
|
app.get("*", (req, res, next) => {
|
|
1456
1374
|
if (req.path.startsWith("/tools/") || req.path.startsWith("/viewer/") ||
|
|
@@ -1497,7 +1415,6 @@ export async function startServer(config) {
|
|
|
1497
1415
|
const mod = getModule();
|
|
1498
1416
|
const initialState = resolveInitialState(mod);
|
|
1499
1417
|
room = new Room(mod.manifest.id, initialState);
|
|
1500
|
-
maybeStartTickEngine();
|
|
1501
1418
|
console.log(` Experience: ${mod.manifest.id}`);
|
|
1502
1419
|
const server = http.createServer(app);
|
|
1503
1420
|
const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD_BYTES });
|
|
@@ -1835,11 +1752,9 @@ export async function startServer(config) {
|
|
|
1835
1752
|
}
|
|
1836
1753
|
debounceTimer = setTimeout(async () => {
|
|
1837
1754
|
console.log(`\nFile changed${filename ? ` (${filename})` : ""}, rebuilding...`);
|
|
1838
|
-
stopTickEngine();
|
|
1839
1755
|
try {
|
|
1840
1756
|
await loadExperience();
|
|
1841
1757
|
room.broadcastToAll({ type: "experience_updated" });
|
|
1842
|
-
maybeStartTickEngine();
|
|
1843
1758
|
smokeTestClientBundle(PORT);
|
|
1844
1759
|
console.log("Hot reload complete.");
|
|
1845
1760
|
}
|
|
@@ -1847,7 +1762,6 @@ export async function startServer(config) {
|
|
|
1847
1762
|
experienceError = toErrorMessage(err);
|
|
1848
1763
|
console.error("Hot reload failed:", toErrorMessage(err));
|
|
1849
1764
|
room.broadcastToAll({ type: "build_error", error: toErrorMessage(err) });
|
|
1850
|
-
maybeStartTickEngine();
|
|
1851
1765
|
}
|
|
1852
1766
|
finally {
|
|
1853
1767
|
if (rebuildingResolve) {
|
package/hooks/logic.js
CHANGED
|
@@ -92,8 +92,7 @@ export function formatPrompt(ctx) {
|
|
|
92
92
|
}
|
|
93
93
|
// ── 3. Events (what happened since last wake-up) ──────
|
|
94
94
|
if (ctx.events && ctx.events.length > 0) {
|
|
95
|
-
const visibleEvents = ctx.events
|
|
96
|
-
e.owner !== "_system");
|
|
95
|
+
const visibleEvents = ctx.events;
|
|
97
96
|
if (visibleEvents.length > 0) {
|
|
98
97
|
for (const e of visibleEvents) {
|
|
99
98
|
const actor = e.role || e.owner || (e.actorId ? e.actorId.split("-")[0] : "?");
|
|
@@ -130,9 +129,7 @@ export function formatPrompt(ctx) {
|
|
|
130
129
|
export function makeDecision(ctx, iteration) {
|
|
131
130
|
if (ctx === null)
|
|
132
131
|
return null;
|
|
133
|
-
const
|
|
134
|
-
e.owner !== "_system") || [];
|
|
135
|
-
const hasEvents = realEvents.length > 0;
|
|
132
|
+
const hasEvents = (ctx.events?.length || 0) > 0;
|
|
136
133
|
const hasError = !!ctx.lastError;
|
|
137
134
|
const hasBrowserErrors = ctx.browserErrors != null && ctx.browserErrors.length > 0;
|
|
138
135
|
const hasObserveError = !!ctx.observeError;
|
package/package.json
CHANGED
package/dist/protocol.d.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protocol experience support.
|
|
3
|
-
*
|
|
4
|
-
* Loads experiences defined as manifest.json + subprocess tool handler.
|
|
5
|
-
* The subprocess communicates via newline-delimited JSON over stdin/stdout.
|
|
6
|
-
*/
|
|
7
|
-
import type { ExperienceModule } from "@vibevibes/sdk";
|
|
8
|
-
export interface ProtocolManifest {
|
|
9
|
-
id: string;
|
|
10
|
-
version: string;
|
|
11
|
-
title: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
initialState?: Record<string, any>;
|
|
14
|
-
tools: ProtocolToolDef[];
|
|
15
|
-
toolProcess: {
|
|
16
|
-
command: string;
|
|
17
|
-
args?: string[];
|
|
18
|
-
};
|
|
19
|
-
agents?: Array<{
|
|
20
|
-
role: string;
|
|
21
|
-
systemPrompt: string;
|
|
22
|
-
allowedTools?: string[];
|
|
23
|
-
autoSpawn?: boolean;
|
|
24
|
-
maxInstances?: number;
|
|
25
|
-
}>;
|
|
26
|
-
observe?: {
|
|
27
|
-
exclude?: string[];
|
|
28
|
-
include?: string[];
|
|
29
|
-
};
|
|
30
|
-
canvas?: string;
|
|
31
|
-
netcode?: "default" | "tick";
|
|
32
|
-
tickRateMs?: number;
|
|
33
|
-
streams?: Array<{
|
|
34
|
-
name: string;
|
|
35
|
-
input_schema: Record<string, any>;
|
|
36
|
-
rateLimit?: number;
|
|
37
|
-
}>;
|
|
38
|
-
}
|
|
39
|
-
interface ProtocolToolDef {
|
|
40
|
-
name: string;
|
|
41
|
-
description: string;
|
|
42
|
-
input_schema: Record<string, any>;
|
|
43
|
-
risk?: "low" | "medium" | "high";
|
|
44
|
-
}
|
|
45
|
-
interface RpcRequest {
|
|
46
|
-
id: string;
|
|
47
|
-
method: "tool" | "observe" | "stream" | "init" | "ping";
|
|
48
|
-
params: Record<string, any>;
|
|
49
|
-
}
|
|
50
|
-
interface RpcResponse {
|
|
51
|
-
id: string;
|
|
52
|
-
result?: Record<string, any>;
|
|
53
|
-
error?: {
|
|
54
|
-
message: string;
|
|
55
|
-
code?: string;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
export declare class SubprocessExecutor {
|
|
59
|
-
private proc;
|
|
60
|
-
private pending;
|
|
61
|
-
private buffer;
|
|
62
|
-
private seq;
|
|
63
|
-
private command;
|
|
64
|
-
private args;
|
|
65
|
-
private cwd;
|
|
66
|
-
private restarting;
|
|
67
|
-
constructor(command: string, args: string[], cwd: string);
|
|
68
|
-
start(): void;
|
|
69
|
-
stop(): void;
|
|
70
|
-
private processBuffer;
|
|
71
|
-
send(method: RpcRequest["method"], params: Record<string, any>, timeoutMs?: number): Promise<RpcResponse>;
|
|
72
|
-
get running(): boolean;
|
|
73
|
-
}
|
|
74
|
-
export declare function isProtocolExperience(entryPath: string): boolean;
|
|
75
|
-
export declare function loadProtocolManifest(manifestPath: string): ProtocolManifest;
|
|
76
|
-
/**
|
|
77
|
-
* Create a synthetic ExperienceModule from a protocol manifest.
|
|
78
|
-
* Tool handlers proxy calls to the subprocess executor.
|
|
79
|
-
*/
|
|
80
|
-
export declare function createProtocolModule(manifest: ProtocolManifest, executor: SubprocessExecutor, manifestDir: string): ExperienceModule & {
|
|
81
|
-
initialState?: Record<string, any>;
|
|
82
|
-
_executor: SubprocessExecutor;
|
|
83
|
-
_canvasPath?: string;
|
|
84
|
-
};
|
|
85
|
-
export {};
|
package/dist/protocol.js
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protocol experience support.
|
|
3
|
-
*
|
|
4
|
-
* Loads experiences defined as manifest.json + subprocess tool handler.
|
|
5
|
-
* The subprocess communicates via newline-delimited JSON over stdin/stdout.
|
|
6
|
-
*/
|
|
7
|
-
import { spawn } from "child_process";
|
|
8
|
-
import * as path from "path";
|
|
9
|
-
import * as fs from "fs";
|
|
10
|
-
// ── Subprocess executor ─────────────────────────────────────────────────
|
|
11
|
-
export class SubprocessExecutor {
|
|
12
|
-
proc = null;
|
|
13
|
-
pending = new Map();
|
|
14
|
-
buffer = "";
|
|
15
|
-
seq = 0;
|
|
16
|
-
command;
|
|
17
|
-
args;
|
|
18
|
-
cwd;
|
|
19
|
-
restarting = false;
|
|
20
|
-
constructor(command, args, cwd) {
|
|
21
|
-
this.command = command;
|
|
22
|
-
this.args = args;
|
|
23
|
-
this.cwd = cwd;
|
|
24
|
-
}
|
|
25
|
-
start() {
|
|
26
|
-
if (this.proc)
|
|
27
|
-
return;
|
|
28
|
-
this.proc = spawn(this.command, this.args, {
|
|
29
|
-
cwd: this.cwd,
|
|
30
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
-
env: { ...process.env },
|
|
32
|
-
});
|
|
33
|
-
this.proc.stdout.on("data", (chunk) => {
|
|
34
|
-
this.buffer += chunk.toString();
|
|
35
|
-
this.processBuffer();
|
|
36
|
-
});
|
|
37
|
-
this.proc.stderr.on("data", (chunk) => {
|
|
38
|
-
const msg = chunk.toString().trim();
|
|
39
|
-
if (msg)
|
|
40
|
-
console.error(`[protocol:${this.command}] ${msg}`);
|
|
41
|
-
});
|
|
42
|
-
this.proc.on("exit", (code) => {
|
|
43
|
-
console.warn(`[protocol] Tool process exited with code ${code}`);
|
|
44
|
-
this.proc = null;
|
|
45
|
-
// Reject all pending requests
|
|
46
|
-
for (const [id, p] of this.pending) {
|
|
47
|
-
p.reject(new Error(`Tool process exited (code ${code})`));
|
|
48
|
-
clearTimeout(p.timer);
|
|
49
|
-
}
|
|
50
|
-
this.pending.clear();
|
|
51
|
-
this.buffer = "";
|
|
52
|
-
// Auto-restart after a short delay
|
|
53
|
-
if (!this.restarting) {
|
|
54
|
-
this.restarting = true;
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
this.restarting = false;
|
|
57
|
-
try {
|
|
58
|
-
this.start();
|
|
59
|
-
}
|
|
60
|
-
catch { }
|
|
61
|
-
}, 1000);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
this.proc.on("error", (err) => {
|
|
65
|
-
console.error(`[protocol] Failed to spawn tool process: ${err.message}`);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
stop() {
|
|
69
|
-
this.restarting = true; // Prevent auto-restart
|
|
70
|
-
if (this.proc) {
|
|
71
|
-
this.proc.kill();
|
|
72
|
-
this.proc = null;
|
|
73
|
-
}
|
|
74
|
-
for (const [, p] of this.pending) {
|
|
75
|
-
p.reject(new Error("Executor stopped"));
|
|
76
|
-
clearTimeout(p.timer);
|
|
77
|
-
}
|
|
78
|
-
this.pending.clear();
|
|
79
|
-
}
|
|
80
|
-
processBuffer() {
|
|
81
|
-
const lines = this.buffer.split("\n");
|
|
82
|
-
this.buffer = lines.pop() || "";
|
|
83
|
-
for (const line of lines) {
|
|
84
|
-
const trimmed = line.trim();
|
|
85
|
-
if (!trimmed)
|
|
86
|
-
continue;
|
|
87
|
-
try {
|
|
88
|
-
const response = JSON.parse(trimmed);
|
|
89
|
-
const pending = this.pending.get(response.id);
|
|
90
|
-
if (pending) {
|
|
91
|
-
clearTimeout(pending.timer);
|
|
92
|
-
this.pending.delete(response.id);
|
|
93
|
-
pending.resolve(response);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
console.warn(`[protocol] Invalid JSON from tool process: ${trimmed.slice(0, 200)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async send(method, params, timeoutMs = 10000) {
|
|
102
|
-
if (!this.proc?.stdin?.writable) {
|
|
103
|
-
throw new Error("Tool process not running");
|
|
104
|
-
}
|
|
105
|
-
const id = `req-${++this.seq}`;
|
|
106
|
-
const request = { id, method, params };
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const timer = setTimeout(() => {
|
|
109
|
-
this.pending.delete(id);
|
|
110
|
-
reject(new Error(`Tool process timeout after ${timeoutMs}ms`));
|
|
111
|
-
}, timeoutMs);
|
|
112
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
113
|
-
try {
|
|
114
|
-
this.proc.stdin.write(JSON.stringify(request) + "\n");
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
clearTimeout(timer);
|
|
118
|
-
this.pending.delete(id);
|
|
119
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
get running() {
|
|
124
|
-
return this.proc !== null && !this.proc.killed;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// ── Load a protocol experience ──────────────────────────────────────────
|
|
128
|
-
export function isProtocolExperience(entryPath) {
|
|
129
|
-
return entryPath.endsWith("manifest.json");
|
|
130
|
-
}
|
|
131
|
-
export function loadProtocolManifest(manifestPath) {
|
|
132
|
-
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
133
|
-
const manifest = JSON.parse(raw);
|
|
134
|
-
if (!manifest.id)
|
|
135
|
-
throw new Error("manifest.json: id is required");
|
|
136
|
-
if (!manifest.version)
|
|
137
|
-
throw new Error("manifest.json: version is required");
|
|
138
|
-
if (!manifest.title)
|
|
139
|
-
throw new Error("manifest.json: title is required");
|
|
140
|
-
if (!manifest.toolProcess)
|
|
141
|
-
throw new Error("manifest.json: toolProcess is required");
|
|
142
|
-
if (!Array.isArray(manifest.tools))
|
|
143
|
-
throw new Error("manifest.json: tools must be an array");
|
|
144
|
-
return manifest;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Create a synthetic ExperienceModule from a protocol manifest.
|
|
148
|
-
* Tool handlers proxy calls to the subprocess executor.
|
|
149
|
-
*/
|
|
150
|
-
export function createProtocolModule(manifest, executor, manifestDir) {
|
|
151
|
-
// Create tool defs with handlers that proxy to the subprocess
|
|
152
|
-
const tools = manifest.tools.map((t) => ({
|
|
153
|
-
name: t.name,
|
|
154
|
-
description: t.description,
|
|
155
|
-
// Use a plain object as input_schema with a parse method for compatibility
|
|
156
|
-
input_schema: createJsonSchemaValidator(t.input_schema),
|
|
157
|
-
risk: (t.risk || "low"),
|
|
158
|
-
capabilities_required: ["state.write"],
|
|
159
|
-
handler: async (ctx, input) => {
|
|
160
|
-
const response = await executor.send("tool", {
|
|
161
|
-
tool: t.name,
|
|
162
|
-
input,
|
|
163
|
-
state: ctx.state,
|
|
164
|
-
actorId: ctx.actorId,
|
|
165
|
-
roomId: ctx.roomId,
|
|
166
|
-
timestamp: ctx.timestamp,
|
|
167
|
-
});
|
|
168
|
-
if (response.error) {
|
|
169
|
-
throw new Error(response.error.message);
|
|
170
|
-
}
|
|
171
|
-
if (response.result?.state) {
|
|
172
|
-
ctx.setState(response.result.state);
|
|
173
|
-
}
|
|
174
|
-
return response.result?.output ?? {};
|
|
175
|
-
},
|
|
176
|
-
}));
|
|
177
|
-
// Build observe function if manifest specifies observation config
|
|
178
|
-
let observe;
|
|
179
|
-
if (manifest.observe) {
|
|
180
|
-
const { exclude, include } = manifest.observe;
|
|
181
|
-
observe = (state, _event, _actorId) => {
|
|
182
|
-
if (include) {
|
|
183
|
-
const filtered = {};
|
|
184
|
-
for (const key of include) {
|
|
185
|
-
if (key in state)
|
|
186
|
-
filtered[key] = state[key];
|
|
187
|
-
}
|
|
188
|
-
return filtered;
|
|
189
|
-
}
|
|
190
|
-
if (exclude) {
|
|
191
|
-
const filtered = { ...state };
|
|
192
|
-
for (const key of exclude) {
|
|
193
|
-
delete filtered[key];
|
|
194
|
-
}
|
|
195
|
-
return filtered;
|
|
196
|
-
}
|
|
197
|
-
return state;
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
// Resolve canvas path
|
|
201
|
-
const canvasFile = manifest.canvas || "index.html";
|
|
202
|
-
const canvasPath = path.resolve(manifestDir, canvasFile);
|
|
203
|
-
const hasCanvas = fs.existsSync(canvasPath);
|
|
204
|
-
return {
|
|
205
|
-
manifest: {
|
|
206
|
-
id: manifest.id,
|
|
207
|
-
version: manifest.version,
|
|
208
|
-
title: manifest.title,
|
|
209
|
-
description: manifest.description || "",
|
|
210
|
-
requested_capabilities: ["state.write"],
|
|
211
|
-
agentSlots: manifest.agents,
|
|
212
|
-
participantSlots: manifest.agents?.map((a) => ({ ...a, type: "ai" })),
|
|
213
|
-
netcode: manifest.netcode,
|
|
214
|
-
tickRateMs: manifest.tickRateMs,
|
|
215
|
-
},
|
|
216
|
-
Canvas: (() => null), // Protocol experiences use HTML canvas, not React
|
|
217
|
-
tools,
|
|
218
|
-
observe,
|
|
219
|
-
initialState: manifest.initialState ?? {},
|
|
220
|
-
_executor: executor,
|
|
221
|
-
_canvasPath: hasCanvas ? canvasPath : undefined,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Create a minimal Zod-compatible validator from a JSON Schema.
|
|
226
|
-
* Only needs .parse() for the server's input validation.
|
|
227
|
-
*/
|
|
228
|
-
function createJsonSchemaValidator(schema) {
|
|
229
|
-
return {
|
|
230
|
-
parse: (input) => {
|
|
231
|
-
// Basic type validation for protocol experiences
|
|
232
|
-
if (schema.type === "object" && typeof input !== "object") {
|
|
233
|
-
throw new Error("Expected object input");
|
|
234
|
-
}
|
|
235
|
-
return input;
|
|
236
|
-
},
|
|
237
|
-
// Expose the raw JSON Schema for tool listing
|
|
238
|
-
_jsonSchema: schema,
|
|
239
|
-
};
|
|
240
|
-
}
|
package/dist/tick-engine.d.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tick Engine — server-side tick loop for experiences with netcode: "tick".
|
|
3
|
-
*
|
|
4
|
-
* Runs at a fixed tick rate, executing tool calls from the tick context.
|
|
5
|
-
* The tick engine provides the timing infrastructure; experiences define
|
|
6
|
-
* what happens each tick via their tool handlers.
|
|
7
|
-
*/
|
|
8
|
-
import type { ToolCtx, ToolEvent } from "@vibevibes/sdk";
|
|
9
|
-
import type { EventEmitter } from "events";
|
|
10
|
-
/** ToolEvent extended with observation field (computed server-side after tool execution). */
|
|
11
|
-
interface TickToolEvent extends ToolEvent {
|
|
12
|
-
observation?: Record<string, unknown>;
|
|
13
|
-
}
|
|
14
|
-
/** Minimal Room interface — what the tick engine needs from a room. */
|
|
15
|
-
export interface TickRoom {
|
|
16
|
-
readonly id: string;
|
|
17
|
-
readonly experienceId: string;
|
|
18
|
-
readonly config: Record<string, unknown>;
|
|
19
|
-
sharedState: Record<string, unknown>;
|
|
20
|
-
participantList(): string[];
|
|
21
|
-
broadcastToAll(message: Record<string, unknown>): void;
|
|
22
|
-
broadcastStateUpdate?(extra: {
|
|
23
|
-
changedBy: string;
|
|
24
|
-
tick?: unknown;
|
|
25
|
-
}, forceFullState?: boolean): void;
|
|
26
|
-
appendEvent(event: TickToolEvent): void;
|
|
27
|
-
enqueueExecution<T>(fn: () => Promise<T>): Promise<T>;
|
|
28
|
-
}
|
|
29
|
-
/** Minimal experience interface — what the tick engine needs. */
|
|
30
|
-
export interface TickExperience {
|
|
31
|
-
module: {
|
|
32
|
-
manifest: {
|
|
33
|
-
id: string;
|
|
34
|
-
};
|
|
35
|
-
tools: Array<{
|
|
36
|
-
name: string;
|
|
37
|
-
input_schema?: {
|
|
38
|
-
parse?: (input: unknown) => unknown;
|
|
39
|
-
};
|
|
40
|
-
handler: (ctx: ToolCtx, input: unknown) => Promise<unknown>;
|
|
41
|
-
}>;
|
|
42
|
-
observe?: (state: Record<string, unknown>, event: TickToolEvent, actorId: string) => Record<string, unknown>;
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
export declare class TickEngine {
|
|
46
|
-
private room;
|
|
47
|
-
private experience;
|
|
48
|
-
private roomEvents;
|
|
49
|
-
private interval;
|
|
50
|
-
private tickCount;
|
|
51
|
-
private tickRateMs;
|
|
52
|
-
private startedAt;
|
|
53
|
-
private _running;
|
|
54
|
-
private _stopped;
|
|
55
|
-
private _stateSetThisTick;
|
|
56
|
-
constructor(room: TickRoom, experience: TickExperience, roomEvents: EventEmitter, tickRateMs?: number);
|
|
57
|
-
/** Start the tick loop. Uses self-correcting setTimeout to prevent timing drift. */
|
|
58
|
-
start(): void;
|
|
59
|
-
/** Schedule the next tick with drift correction. */
|
|
60
|
-
private scheduleNext;
|
|
61
|
-
/** Stop the tick loop. */
|
|
62
|
-
stop(): void;
|
|
63
|
-
/** Mark dirty — no-op in simplified engine, kept for API compat. */
|
|
64
|
-
markDirty(): void;
|
|
65
|
-
/** Get current tick status. */
|
|
66
|
-
getStatus(): {
|
|
67
|
-
enabled: boolean;
|
|
68
|
-
tickRateMs: number;
|
|
69
|
-
tickCount: number;
|
|
70
|
-
};
|
|
71
|
-
/** Execute one tick cycle. */
|
|
72
|
-
private tick;
|
|
73
|
-
/** Execute a tool call internally (no HTTP round-trip). */
|
|
74
|
-
executeTool(toolName: string, input: Record<string, unknown>, callerActorId?: string, expiredFlag?: {
|
|
75
|
-
value: boolean;
|
|
76
|
-
}): Promise<{
|
|
77
|
-
output?: unknown;
|
|
78
|
-
error?: string;
|
|
79
|
-
}>;
|
|
80
|
-
}
|
|
81
|
-
export {};
|
package/dist/tick-engine.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tick Engine — server-side tick loop for experiences with netcode: "tick".
|
|
3
|
-
*
|
|
4
|
-
* Runs at a fixed tick rate, executing tool calls from the tick context.
|
|
5
|
-
* The tick engine provides the timing infrastructure; experiences define
|
|
6
|
-
* what happens each tick via their tool handlers.
|
|
7
|
-
*/
|
|
8
|
-
// ── Tick Engine ───────────────────────────────────────────────────────────────
|
|
9
|
-
const TICK_ACTOR_ID = "_tick-engine";
|
|
10
|
-
const TICK_OWNER = "_system";
|
|
11
|
-
export class TickEngine {
|
|
12
|
-
room;
|
|
13
|
-
experience;
|
|
14
|
-
roomEvents;
|
|
15
|
-
interval = null;
|
|
16
|
-
tickCount = 0;
|
|
17
|
-
tickRateMs;
|
|
18
|
-
startedAt = 0;
|
|
19
|
-
_running = false;
|
|
20
|
-
_stopped = false;
|
|
21
|
-
_stateSetThisTick = false;
|
|
22
|
-
constructor(room, experience, roomEvents, tickRateMs = 50) {
|
|
23
|
-
this.room = room;
|
|
24
|
-
this.experience = experience;
|
|
25
|
-
this.roomEvents = roomEvents;
|
|
26
|
-
this.tickRateMs = tickRateMs;
|
|
27
|
-
}
|
|
28
|
-
/** Start the tick loop. Uses self-correcting setTimeout to prevent timing drift. */
|
|
29
|
-
start() {
|
|
30
|
-
if (this.interval)
|
|
31
|
-
return;
|
|
32
|
-
this._stopped = false;
|
|
33
|
-
this.tickCount = 0;
|
|
34
|
-
this.startedAt = Date.now();
|
|
35
|
-
this.scheduleNext();
|
|
36
|
-
}
|
|
37
|
-
/** Schedule the next tick with drift correction. */
|
|
38
|
-
scheduleNext() {
|
|
39
|
-
if (this._stopped)
|
|
40
|
-
return;
|
|
41
|
-
const expected = this.startedAt + (this.tickCount + 1) * this.tickRateMs;
|
|
42
|
-
const delay = Math.max(0, expected - Date.now());
|
|
43
|
-
this.interval = setTimeout(() => {
|
|
44
|
-
this.tick().then(() => this.scheduleNext(), () => this.scheduleNext());
|
|
45
|
-
}, delay);
|
|
46
|
-
}
|
|
47
|
-
/** Stop the tick loop. */
|
|
48
|
-
stop() {
|
|
49
|
-
this._stopped = true;
|
|
50
|
-
this._running = false;
|
|
51
|
-
if (this.interval) {
|
|
52
|
-
clearTimeout(this.interval);
|
|
53
|
-
this.interval = null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/** Mark dirty — no-op in simplified engine, kept for API compat. */
|
|
57
|
-
markDirty() { }
|
|
58
|
-
/** Get current tick status. */
|
|
59
|
-
getStatus() {
|
|
60
|
-
return {
|
|
61
|
-
enabled: this.interval !== null,
|
|
62
|
-
tickRateMs: this.tickRateMs,
|
|
63
|
-
tickCount: this.tickCount,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** Execute one tick cycle. */
|
|
67
|
-
async tick() {
|
|
68
|
-
if (this._running)
|
|
69
|
-
return;
|
|
70
|
-
this._running = true;
|
|
71
|
-
try {
|
|
72
|
-
this.tickCount++;
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
this._running = false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/** Execute a tool call internally (no HTTP round-trip). */
|
|
79
|
-
async executeTool(toolName, input, callerActorId, expiredFlag) {
|
|
80
|
-
const tool = this.experience.module.tools.find((t) => t.name === toolName);
|
|
81
|
-
if (!tool) {
|
|
82
|
-
return { error: `Tool '${toolName}' not found` };
|
|
83
|
-
}
|
|
84
|
-
const actorId = callerActorId || TICK_ACTOR_ID;
|
|
85
|
-
const owner = callerActorId || TICK_OWNER;
|
|
86
|
-
try {
|
|
87
|
-
let validatedInput = input;
|
|
88
|
-
if (tool.input_schema?.parse) {
|
|
89
|
-
validatedInput = tool.input_schema.parse(input);
|
|
90
|
-
}
|
|
91
|
-
const self = this;
|
|
92
|
-
const ctxBase = {
|
|
93
|
-
roomId: this.room.id,
|
|
94
|
-
actorId,
|
|
95
|
-
owner,
|
|
96
|
-
state: null,
|
|
97
|
-
setState: (newState) => {
|
|
98
|
-
if (expiredFlag?.value)
|
|
99
|
-
return;
|
|
100
|
-
self.room.sharedState = newState;
|
|
101
|
-
self._stateSetThisTick = true;
|
|
102
|
-
},
|
|
103
|
-
timestamp: Date.now(),
|
|
104
|
-
memory: {},
|
|
105
|
-
setMemory: () => { },
|
|
106
|
-
};
|
|
107
|
-
Object.defineProperty(ctxBase, 'state', {
|
|
108
|
-
get() { return self.room.sharedState; },
|
|
109
|
-
enumerable: true,
|
|
110
|
-
configurable: true,
|
|
111
|
-
});
|
|
112
|
-
const ctx = ctxBase;
|
|
113
|
-
const output = await tool.handler(ctx, validatedInput);
|
|
114
|
-
const event = {
|
|
115
|
-
id: `tick-${this.tickCount}-${toolName}-${Math.random().toString(36).slice(2, 6)}`,
|
|
116
|
-
ts: Date.now(),
|
|
117
|
-
actorId: TICK_ACTOR_ID,
|
|
118
|
-
owner: TICK_OWNER,
|
|
119
|
-
tool: toolName,
|
|
120
|
-
input: validatedInput,
|
|
121
|
-
output,
|
|
122
|
-
};
|
|
123
|
-
if (this.experience.module.observe) {
|
|
124
|
-
try {
|
|
125
|
-
event.observation = this.experience.module.observe(this.room.sharedState, event, actorId);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
// Don't fail tick if observe throws
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
this.room.appendEvent(event);
|
|
132
|
-
this.roomEvents.emit(`room:${this.room.id}`);
|
|
133
|
-
return { output };
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
137
|
-
const event = {
|
|
138
|
-
id: `tick-${this.tickCount}-${toolName}-${Math.random().toString(36).slice(2, 6)}`,
|
|
139
|
-
ts: Date.now(),
|
|
140
|
-
actorId: TICK_ACTOR_ID,
|
|
141
|
-
owner: TICK_OWNER,
|
|
142
|
-
tool: toolName,
|
|
143
|
-
input,
|
|
144
|
-
error: errorMsg,
|
|
145
|
-
};
|
|
146
|
-
this.room.appendEvent(event);
|
|
147
|
-
this.roomEvents.emit(`room:${this.room.id}`);
|
|
148
|
-
return { error: errorMsg };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|