@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 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 a manifest.json (protocol) or src/index.tsx (TypeScript).`);
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.filter((e) => e.actorId !== "_tick-engine" &&
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 realEvents = ctx.events?.filter((e) => e.actorId !== "_tick-engine" &&
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibevibes/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server + runtime engine for vibevibes experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- }
@@ -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 {};
@@ -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
- }