@vibevibes/mcp 0.5.0 → 0.7.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/index.js +0 -26
- package/dist/server.js +2 -535
- 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/index.js
CHANGED
|
@@ -423,28 +423,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
423
423
|
}
|
|
424
424
|
throw stateErr;
|
|
425
425
|
}
|
|
426
|
-
// Fetch slot definitions to detect unfilled autoSpawn AI slots
|
|
427
|
-
let unfilledSlots = [];
|
|
428
|
-
try {
|
|
429
|
-
const slotsData = await fetchJSON("/slots", { timeoutMs: 5000 });
|
|
430
|
-
if (slotsData?.slots) {
|
|
431
|
-
const currentParticipants = slotsData.participantDetails || [];
|
|
432
|
-
for (const slot of slotsData.slots) {
|
|
433
|
-
if (slot.type !== "ai" && slot.type !== "any")
|
|
434
|
-
continue;
|
|
435
|
-
if (!slot.autoSpawn)
|
|
436
|
-
continue;
|
|
437
|
-
const max = slot.maxInstances ?? 1;
|
|
438
|
-
const filled = currentParticipants.filter(p => p.role === slot.role && p.type === "ai").length;
|
|
439
|
-
if (filled < max) {
|
|
440
|
-
unfilledSlots.push({ role: slot.role, autoSpawn: slot.autoSpawn, maxInstances: max });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
// Slots endpoint not available — skip silently
|
|
447
|
-
}
|
|
448
426
|
const outputParts = [
|
|
449
427
|
`Connected as ${identity.actorId}`,
|
|
450
428
|
connections.size > 1
|
|
@@ -461,10 +439,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
461
439
|
outputParts.push(`Your instructions:`, systemPrompt, ``);
|
|
462
440
|
}
|
|
463
441
|
outputParts.push(formatState(state.sharedState, joinData?.observation || state.observation), `Participants: ${state.participants?.join(", ")}`, ``, `Tools:`, formatToolList(joinData?.tools || state.tools || []), ``);
|
|
464
|
-
// Surface unfilled autoSpawn slots
|
|
465
|
-
if (unfilledSlots.length > 0) {
|
|
466
|
-
outputParts.push(`UNFILLED AI SLOTS (autoSpawn):`, ...unfilledSlots.map(s => ` - ${s.role} (max ${s.maxInstances ?? 1})`), ``, `These roles need AI agents. Spawn independent teammates — each gets its own MCP session and identity.`, ` Agent(subagent_type="general-purpose", model="sonnet", run_in_background=true,`, ` prompt="Call connect with role='ROLE'. Then play your role.")`, ``);
|
|
467
|
-
}
|
|
468
442
|
outputParts.push(`Use act() to interact. The stop hook keeps you present.`);
|
|
469
443
|
// Register the stop hook so Claude Code wakes us on events
|
|
470
444
|
ensureStopHook();
|
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";
|
|
@@ -217,14 +215,8 @@ const DEFAULT_PORT = 4321;
|
|
|
217
215
|
const MAX_EVENTS = 200;
|
|
218
216
|
const JOIN_EVENT_HISTORY = 20;
|
|
219
217
|
const ROOM_STATE_EVENT_HISTORY = 50;
|
|
220
|
-
const HISTORY_DEFAULT_LIMIT = 50;
|
|
221
|
-
const HISTORY_MAX_LIMIT = 200;
|
|
222
|
-
const DEFAULT_STREAM_RATE_LIMIT = 60;
|
|
223
|
-
const STREAM_RATE_WINDOW_MS = 1000;
|
|
224
218
|
const EVENT_BATCH_DEBOUNCE_MS = 50;
|
|
225
|
-
const DEFAULT_TICK_RATE_MS = 50;
|
|
226
219
|
const MAX_BATCH_CALLS = 10;
|
|
227
|
-
const LONG_POLL_MAX_TIMEOUT_MS = 55000;
|
|
228
220
|
const AGENT_CONTEXT_MAX_TIMEOUT_MS = 10000;
|
|
229
221
|
const WS_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
230
222
|
const WS_EPHEMERAL_MAX_BYTES = 65536;
|
|
@@ -233,10 +225,7 @@ const HOT_RELOAD_DEBOUNCE_MS = 300;
|
|
|
233
225
|
const WS_CLOSE_GRACE_MS = 3000;
|
|
234
226
|
const JSON_BODY_LIMIT = "256kb";
|
|
235
227
|
const TOOL_HTTP_TIMEOUT_MS = 30_000;
|
|
236
|
-
const TOOL_REGEX_MAX_LENGTH = 100;
|
|
237
228
|
const ROOM_EVENTS_MAX_LISTENERS = 200;
|
|
238
|
-
const STREAM_RATE_LIMIT_STALE_MS = 5000;
|
|
239
|
-
const STREAM_RATE_LIMIT_CLEANUP_INTERVAL_MS = 10000;
|
|
240
229
|
const IDEMPOTENCY_CLEANUP_INTERVAL_MS = 60000;
|
|
241
230
|
// ── Default observe ────────────────────────────────────────
|
|
242
231
|
function defaultObserve(state, _event, _actorId) {
|
|
@@ -253,7 +242,6 @@ function defaultObserve(state, _event, _actorId) {
|
|
|
253
242
|
let PORT = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
|
|
254
243
|
let publicUrl = null;
|
|
255
244
|
let room;
|
|
256
|
-
let tickEngine = null;
|
|
257
245
|
let _actorCounter = 0;
|
|
258
246
|
const roomEvents = new EventEmitter();
|
|
259
247
|
roomEvents.setMaxListeners(ROOM_EVENTS_MAX_LISTENERS);
|
|
@@ -263,15 +251,6 @@ let experienceError = null;
|
|
|
263
251
|
// Hot-reload rebuild gate
|
|
264
252
|
let rebuildingResolve = null;
|
|
265
253
|
let rebuildingPromise = null;
|
|
266
|
-
// Stream rate limiting
|
|
267
|
-
const streamRateLimits = new Map();
|
|
268
|
-
const _streamRateCleanupTimer = setInterval(() => {
|
|
269
|
-
const now = Date.now();
|
|
270
|
-
for (const [key, entry] of streamRateLimits) {
|
|
271
|
-
if (now - entry.windowStart > STREAM_RATE_LIMIT_STALE_MS)
|
|
272
|
-
streamRateLimits.delete(key);
|
|
273
|
-
}
|
|
274
|
-
}, STREAM_RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
275
254
|
export function setPublicUrl(url) {
|
|
276
255
|
publicUrl = url;
|
|
277
256
|
}
|
|
@@ -345,9 +324,6 @@ function experienceNotLoadedError() {
|
|
|
345
324
|
}
|
|
346
325
|
// ── Experience discovery & loading ──────────────────────────
|
|
347
326
|
function discoverEntryPath() {
|
|
348
|
-
const manifestPath = path.join(PROJECT_ROOT, "manifest.json");
|
|
349
|
-
if (fs.existsSync(manifestPath))
|
|
350
|
-
return manifestPath;
|
|
351
327
|
const tsxPath = path.join(PROJECT_ROOT, "src", "index.tsx");
|
|
352
328
|
if (fs.existsSync(tsxPath))
|
|
353
329
|
return tsxPath;
|
|
@@ -355,15 +331,10 @@ function discoverEntryPath() {
|
|
|
355
331
|
if (fs.existsSync(rootTsx))
|
|
356
332
|
return rootTsx;
|
|
357
333
|
throw new Error(`No experience found in ${PROJECT_ROOT}. ` +
|
|
358
|
-
`Create
|
|
334
|
+
`Create src/index.tsx (TypeScript).`);
|
|
359
335
|
}
|
|
360
|
-
const protocolExecutors = new Map();
|
|
361
336
|
async function loadExperience() {
|
|
362
337
|
const entryPath = discoverEntryPath();
|
|
363
|
-
if (isProtocolExperience(entryPath)) {
|
|
364
|
-
await loadProtocolExperience(entryPath);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
338
|
const [sCode, cCode] = await Promise.all([
|
|
368
339
|
bundleForServer(entryPath),
|
|
369
340
|
bundleForClient(entryPath),
|
|
@@ -385,48 +356,6 @@ async function loadExperience() {
|
|
|
385
356
|
};
|
|
386
357
|
experienceError = null;
|
|
387
358
|
}
|
|
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
359
|
// ── Express app ────────────────────────────────────────────
|
|
431
360
|
const app = express();
|
|
432
361
|
app.use(express.json({ limit: JSON_BODY_LIMIT }));
|
|
@@ -475,12 +404,6 @@ app.get("/state", (req, res) => {
|
|
|
475
404
|
observation,
|
|
476
405
|
});
|
|
477
406
|
});
|
|
478
|
-
// ── Slots endpoint ──────────────────────────────────────────
|
|
479
|
-
app.get("/slots", (_req, res) => {
|
|
480
|
-
const mod = getModule();
|
|
481
|
-
const slots = mod?.manifest?.participantSlots || mod?.participants || [];
|
|
482
|
-
res.json({ slots, participantDetails: room.participantDetails() });
|
|
483
|
-
});
|
|
484
407
|
// ── Participants endpoint ──────────────────────────────────
|
|
485
408
|
app.get("/participants", (_req, res) => res.json({ participants: room.participantDetails() }));
|
|
486
409
|
// ── Tools list endpoint ────────────────────────────────────
|
|
@@ -497,79 +420,10 @@ app.get("/tools-list", (req, res) => {
|
|
|
497
420
|
allowedTools = participant?.allowedTools;
|
|
498
421
|
}
|
|
499
422
|
const tools = getToolList(mod, allowedTools);
|
|
500
|
-
const streams = mod.streams
|
|
501
|
-
? mod.streams.map((s) => ({
|
|
502
|
-
name: s.name,
|
|
503
|
-
description: s.description || "",
|
|
504
|
-
rateLimit: s.rateLimit || DEFAULT_STREAM_RATE_LIMIT,
|
|
505
|
-
input_schema: s.input_schema ? zodToJsonSchema(s.input_schema) : {},
|
|
506
|
-
}))
|
|
507
|
-
: [];
|
|
508
423
|
res.json({
|
|
509
424
|
experienceId: mod.manifest?.id,
|
|
510
425
|
tools,
|
|
511
|
-
streams,
|
|
512
426
|
toolCount: tools.length,
|
|
513
|
-
streamCount: streams.length,
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
// ── History endpoint ───────────────────────────────────────
|
|
517
|
-
app.get("/history", (req, res) => {
|
|
518
|
-
const limit = Math.min(queryInt(req.query.limit) || HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT);
|
|
519
|
-
const since = queryInt(req.query.since);
|
|
520
|
-
const actor = queryString(req.query.actor);
|
|
521
|
-
const tool = queryString(req.query.tool);
|
|
522
|
-
const owner = queryString(req.query.owner);
|
|
523
|
-
let events = room.events;
|
|
524
|
-
if (since > 0)
|
|
525
|
-
events = events.filter(e => e.ts > since);
|
|
526
|
-
if (actor)
|
|
527
|
-
events = events.filter(e => e.actorId === actor);
|
|
528
|
-
if (owner)
|
|
529
|
-
events = events.filter(e => e.owner === owner);
|
|
530
|
-
if (tool) {
|
|
531
|
-
const SAFE_TOOL_REGEX = /^[a-zA-Z0-9._:\-]+$/;
|
|
532
|
-
if (tool.length <= TOOL_REGEX_MAX_LENGTH && SAFE_TOOL_REGEX.test(tool)) {
|
|
533
|
-
try {
|
|
534
|
-
const escaped = tool.replace(/\./g, '\\.');
|
|
535
|
-
const toolRegex = new RegExp('^' + escaped + '$');
|
|
536
|
-
events = events.filter(e => toolRegex.test(e.tool));
|
|
537
|
-
}
|
|
538
|
-
catch {
|
|
539
|
-
events = events.filter(e => e.tool === tool);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
events = events.filter(e => e.tool === tool);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const filteredTotal = events.length;
|
|
547
|
-
const result = events.slice(-limit);
|
|
548
|
-
res.json({
|
|
549
|
-
events: result,
|
|
550
|
-
total: room.events.length,
|
|
551
|
-
filtered: filteredTotal,
|
|
552
|
-
returned: result.length,
|
|
553
|
-
hasMore: filteredTotal > limit,
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
// ── Who endpoint ───────────────────────────────────────────
|
|
557
|
-
app.get("/who", (_req, res) => {
|
|
558
|
-
const participants = Array.from(room.participants.entries()).map(([actorId, p]) => {
|
|
559
|
-
let lastAction;
|
|
560
|
-
for (let i = room.events.length - 1; i >= 0; i--) {
|
|
561
|
-
if (room.events[i].actorId === actorId) {
|
|
562
|
-
lastAction = { tool: room.events[i].tool, ts: room.events[i].ts };
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return { actorId, owner: p.owner, type: p.type, role: p.role, joinedAt: p.joinedAt, lastAction, allowedTools: p.allowedTools };
|
|
567
|
-
});
|
|
568
|
-
res.json({
|
|
569
|
-
participants,
|
|
570
|
-
count: participants.length,
|
|
571
|
-
humans: participants.filter(p => p.type === "human").length,
|
|
572
|
-
agents: participants.filter(p => p.type === "ai").length,
|
|
573
427
|
});
|
|
574
428
|
});
|
|
575
429
|
// ── Join ───────────────────────────────────────────────────
|
|
@@ -774,53 +628,6 @@ app.post("/leave", (req, res) => {
|
|
|
774
628
|
broadcastPresenceUpdate();
|
|
775
629
|
res.json({ left: true, actorId });
|
|
776
630
|
});
|
|
777
|
-
// ── Kick ───────────────────────────────────────────────────
|
|
778
|
-
app.post("/kick", (req, res) => {
|
|
779
|
-
const { kickerActorId, targetActorId } = req.body;
|
|
780
|
-
if (kickerActorId === targetActorId) {
|
|
781
|
-
res.status(400).json({ error: "Cannot kick yourself" });
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
const kicker = room.participants.get(kickerActorId);
|
|
785
|
-
if (!kicker || kicker.type !== "human") {
|
|
786
|
-
res.status(400).json({ error: "Only human participants can kick" });
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (!room.participants.has(targetActorId)) {
|
|
790
|
-
res.status(400).json({ error: "Participant not found" });
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const targetParticipant = room.participants.get(targetActorId);
|
|
794
|
-
room.participants.delete(targetActorId);
|
|
795
|
-
if (room.kickedActors.size >= 500) {
|
|
796
|
-
const oldest = room.kickedActors.values().next().value;
|
|
797
|
-
if (oldest)
|
|
798
|
-
room.kickedActors.delete(oldest);
|
|
799
|
-
}
|
|
800
|
-
room.kickedActors.add(targetActorId);
|
|
801
|
-
if (targetParticipant?.owner) {
|
|
802
|
-
if (room.kickedOwners.size >= 500) {
|
|
803
|
-
const oldest = room.kickedOwners.values().next().value;
|
|
804
|
-
if (oldest)
|
|
805
|
-
room.kickedOwners.delete(oldest);
|
|
806
|
-
}
|
|
807
|
-
room.kickedOwners.add(targetParticipant.owner);
|
|
808
|
-
}
|
|
809
|
-
for (const [targetWs, wsActorId] of room.wsConnections.entries()) {
|
|
810
|
-
if (wsActorId === targetActorId) {
|
|
811
|
-
try {
|
|
812
|
-
targetWs.send(JSON.stringify({ type: "kicked", by: kickerActorId }));
|
|
813
|
-
targetWs.close();
|
|
814
|
-
}
|
|
815
|
-
catch { }
|
|
816
|
-
room.wsConnections.delete(targetWs);
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
broadcastPresenceUpdate();
|
|
821
|
-
roomEvents.emit("room");
|
|
822
|
-
res.json({ kicked: true, actorId: targetActorId });
|
|
823
|
-
});
|
|
824
631
|
// ── Idempotency cache ────────────────────────────────────────
|
|
825
632
|
const idempotencyCache = new Map();
|
|
826
633
|
const IDEMPOTENCY_TTL = 30000;
|
|
@@ -1075,80 +882,6 @@ app.post("/tools-batch", async (req, res) => {
|
|
|
1075
882
|
});
|
|
1076
883
|
res.status(hasError ? 207 : 200).json({ results, observation: lastObservation });
|
|
1077
884
|
});
|
|
1078
|
-
// ── Events (supports long-poll via ?timeout=N) ──────────────
|
|
1079
|
-
app.get("/events", (req, res) => {
|
|
1080
|
-
const since = queryInt(req.query.since);
|
|
1081
|
-
const timeout = Math.min(queryInt(req.query.timeout), LONG_POLL_MAX_TIMEOUT_MS);
|
|
1082
|
-
const requestingActorId = queryString(req.query.actorId);
|
|
1083
|
-
const computeObservation = (events) => {
|
|
1084
|
-
const mod = getModule();
|
|
1085
|
-
if (!requestingActorId)
|
|
1086
|
-
return undefined;
|
|
1087
|
-
const agentObserve = mod?.observe ?? defaultObserve;
|
|
1088
|
-
try {
|
|
1089
|
-
const lastEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
1090
|
-
return agentObserve(room.sharedState, lastEvent, requestingActorId);
|
|
1091
|
-
}
|
|
1092
|
-
catch (e) {
|
|
1093
|
-
console.error(`[observe] Error:`, toErrorMessage(e));
|
|
1094
|
-
return undefined;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
const getNewEvents = () => room.events.filter((e) => e.ts > since && e.actorId !== requestingActorId);
|
|
1098
|
-
let newEvents = getNewEvents();
|
|
1099
|
-
if (newEvents.length > 0 || timeout === 0) {
|
|
1100
|
-
res.json({
|
|
1101
|
-
events: newEvents,
|
|
1102
|
-
sharedState: room.sharedState,
|
|
1103
|
-
participants: room.participantList(),
|
|
1104
|
-
observation: computeObservation(newEvents),
|
|
1105
|
-
});
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
let responded = false;
|
|
1109
|
-
let batchTimer = null;
|
|
1110
|
-
const respond = () => {
|
|
1111
|
-
if (responded)
|
|
1112
|
-
return;
|
|
1113
|
-
responded = true;
|
|
1114
|
-
clearTimeout(timer);
|
|
1115
|
-
if (batchTimer) {
|
|
1116
|
-
clearTimeout(batchTimer);
|
|
1117
|
-
batchTimer = null;
|
|
1118
|
-
}
|
|
1119
|
-
roomEvents.removeListener("room", onEvent);
|
|
1120
|
-
newEvents = getNewEvents();
|
|
1121
|
-
res.json({
|
|
1122
|
-
events: newEvents,
|
|
1123
|
-
sharedState: room.sharedState,
|
|
1124
|
-
participants: room.participantList(),
|
|
1125
|
-
observation: computeObservation(newEvents),
|
|
1126
|
-
});
|
|
1127
|
-
};
|
|
1128
|
-
const timer = setTimeout(respond, timeout);
|
|
1129
|
-
const onEvent = () => {
|
|
1130
|
-
if (responded)
|
|
1131
|
-
return;
|
|
1132
|
-
if (batchTimer)
|
|
1133
|
-
return;
|
|
1134
|
-
batchTimer = setTimeout(() => {
|
|
1135
|
-
batchTimer = null;
|
|
1136
|
-
if (responded)
|
|
1137
|
-
return;
|
|
1138
|
-
const pending = getNewEvents();
|
|
1139
|
-
if (pending.length > 0)
|
|
1140
|
-
respond();
|
|
1141
|
-
}, EVENT_BATCH_DEBOUNCE_MS);
|
|
1142
|
-
};
|
|
1143
|
-
roomEvents.on("room", onEvent);
|
|
1144
|
-
req.on("close", () => {
|
|
1145
|
-
responded = true;
|
|
1146
|
-
clearTimeout(timer);
|
|
1147
|
-
if (batchTimer)
|
|
1148
|
-
clearTimeout(batchTimer);
|
|
1149
|
-
roomEvents.removeListener("room", onEvent);
|
|
1150
|
-
});
|
|
1151
|
-
});
|
|
1152
885
|
// ── Browser error capture ──────────────────────────────────
|
|
1153
886
|
const browserErrors = [];
|
|
1154
887
|
const MAX_BROWSER_ERRORS = 20;
|
|
@@ -1186,8 +919,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1186
919
|
return room.events.filter(e => {
|
|
1187
920
|
if (requestingOwner && e.owner === requestingOwner)
|
|
1188
921
|
return false;
|
|
1189
|
-
if (e.actorId === "_tick-engine" || e.owner === "_system")
|
|
1190
|
-
return false;
|
|
1191
922
|
return e.ts > since;
|
|
1192
923
|
}).sort((a, b) => a.ts - b.ts);
|
|
1193
924
|
};
|
|
@@ -1239,7 +970,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1239
970
|
lastError,
|
|
1240
971
|
browserErrors: recentBrowserErrors.length > 0 ? recentBrowserErrors : undefined,
|
|
1241
972
|
participants: room.participantList(),
|
|
1242
|
-
tickEngine: tickEngine ? { enabled: true, tickCount: tickEngine.getStatus().tickCount } : undefined,
|
|
1243
973
|
eventCursor,
|
|
1244
974
|
};
|
|
1245
975
|
};
|
|
@@ -1296,165 +1026,10 @@ app.get("/bundle", async (_req, res) => {
|
|
|
1296
1026
|
setNoCacheHeaders(res);
|
|
1297
1027
|
res.send(loadedExperience?.clientBundle || "");
|
|
1298
1028
|
});
|
|
1299
|
-
// ── Stream endpoints ───────────────────────────────────────
|
|
1300
|
-
app.post("/streams/:streamName", (req, res) => {
|
|
1301
|
-
const mod = getModule();
|
|
1302
|
-
if (!mod?.streams) {
|
|
1303
|
-
res.status(404).json({ error: "No streams defined" });
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const streamDef = mod.streams.find((s) => s.name === req.params.streamName);
|
|
1307
|
-
if (!streamDef) {
|
|
1308
|
-
res.status(404).json({ error: `Stream '${req.params.streamName}' not found` });
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
const { actorId, input } = req.body;
|
|
1312
|
-
if (!actorId) {
|
|
1313
|
-
res.status(400).json({ error: "actorId is required for stream updates" });
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
if (!room.participants.has(actorId)) {
|
|
1317
|
-
res.status(403).json({ error: `Actor '${actorId}' is not a participant. Call /join first.` });
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
const rateLimitKey = `${actorId}:${req.params.streamName}`;
|
|
1321
|
-
const now = Date.now();
|
|
1322
|
-
const rateLimit = streamDef.rateLimit || DEFAULT_STREAM_RATE_LIMIT;
|
|
1323
|
-
if (!streamRateLimits.has(rateLimitKey)) {
|
|
1324
|
-
streamRateLimits.set(rateLimitKey, { count: 0, windowStart: now });
|
|
1325
|
-
}
|
|
1326
|
-
const rl = streamRateLimits.get(rateLimitKey);
|
|
1327
|
-
if (now - rl.windowStart > STREAM_RATE_WINDOW_MS) {
|
|
1328
|
-
rl.count = 0;
|
|
1329
|
-
rl.windowStart = now;
|
|
1330
|
-
}
|
|
1331
|
-
if (rl.count >= rateLimit) {
|
|
1332
|
-
res.status(429).json({ error: `Rate limited: max ${rateLimit} updates/sec for stream '${req.params.streamName}'.` });
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
rl.count++;
|
|
1336
|
-
let validatedInput = input;
|
|
1337
|
-
if (streamDef.input_schema?.parse) {
|
|
1338
|
-
try {
|
|
1339
|
-
validatedInput = streamDef.input_schema.parse(input);
|
|
1340
|
-
}
|
|
1341
|
-
catch (err) {
|
|
1342
|
-
res.status(400).json({ error: toErrorMessage(err) });
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
try {
|
|
1347
|
-
room.sharedState = streamDef.merge(room.sharedState, validatedInput, actorId);
|
|
1348
|
-
}
|
|
1349
|
-
catch (err) {
|
|
1350
|
-
res.status(400).json({ error: toErrorMessage(err) });
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
room.broadcastStateUpdate({ changedBy: actorId, stream: req.params.streamName });
|
|
1354
|
-
roomEvents.emit("room");
|
|
1355
|
-
res.json({ ok: true });
|
|
1356
|
-
});
|
|
1357
|
-
// ── Reset ──────────────────────────────────────────────────
|
|
1358
|
-
app.post("/reset", (_req, res) => {
|
|
1359
|
-
const mod = getModule();
|
|
1360
|
-
if (!mod) {
|
|
1361
|
-
res.status(500).json({ error: experienceNotLoadedError() });
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
room.sharedState = resolveInitialState(mod);
|
|
1365
|
-
room.events.length = 0;
|
|
1366
|
-
for (const [, p] of room.participants) {
|
|
1367
|
-
p.eventCursor = 0;
|
|
1368
|
-
}
|
|
1369
|
-
const resetEvent = {
|
|
1370
|
-
id: `${Date.now()}-system-${Math.random().toString(36).slice(2, 6)}`,
|
|
1371
|
-
ts: Date.now(),
|
|
1372
|
-
actorId: "system",
|
|
1373
|
-
tool: "_reset",
|
|
1374
|
-
input: {},
|
|
1375
|
-
output: { reset: true },
|
|
1376
|
-
};
|
|
1377
|
-
room.appendEvent(resetEvent);
|
|
1378
|
-
roomEvents.emit("room");
|
|
1379
|
-
room.resetDeltaTracking();
|
|
1380
|
-
room.broadcastStateUpdate({ changedBy: "system", tool: "_reset" }, true);
|
|
1381
|
-
res.json({ ok: true });
|
|
1382
|
-
});
|
|
1383
|
-
// ── Sync (re-bundle) ──────────────────────────────────────
|
|
1384
|
-
app.post("/sync", async (_req, res) => {
|
|
1385
|
-
try {
|
|
1386
|
-
stopTickEngine();
|
|
1387
|
-
await loadExperience();
|
|
1388
|
-
room.broadcastToAll({ type: "experience_updated" });
|
|
1389
|
-
maybeStartTickEngine();
|
|
1390
|
-
const mod = getModule();
|
|
1391
|
-
res.json({ synced: true, title: mod?.manifest?.title });
|
|
1392
|
-
}
|
|
1393
|
-
catch (err) {
|
|
1394
|
-
res.status(500).json({ error: toErrorMessage(err) });
|
|
1395
|
-
}
|
|
1396
|
-
});
|
|
1397
|
-
// ── Experiences endpoint ───────────────────────────────────
|
|
1398
|
-
app.get("/experiences", async (_req, res) => {
|
|
1399
|
-
const mod = getModule();
|
|
1400
|
-
if (!mod) {
|
|
1401
|
-
res.json([]);
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
res.json([{
|
|
1405
|
-
id: mod.manifest.id,
|
|
1406
|
-
title: mod.manifest.title,
|
|
1407
|
-
description: mod.manifest.description,
|
|
1408
|
-
version: mod.manifest.version,
|
|
1409
|
-
loaded: true,
|
|
1410
|
-
category: mod.manifest.category,
|
|
1411
|
-
tags: mod.manifest.tags,
|
|
1412
|
-
}]);
|
|
1413
|
-
});
|
|
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
|
-
// ── MCP config ─────────────────────────────────────────────
|
|
1423
|
-
app.get("/mcp-config", (_req, res) => {
|
|
1424
|
-
const serverUrl = getBaseUrl();
|
|
1425
|
-
res.json({
|
|
1426
|
-
mcpServers: {
|
|
1427
|
-
"vibevibes-remote": {
|
|
1428
|
-
command: "npx",
|
|
1429
|
-
args: ["-y", "@vibevibes/mcp@latest"],
|
|
1430
|
-
env: { VIBEVIBES_SERVER_URL: serverUrl },
|
|
1431
|
-
},
|
|
1432
|
-
},
|
|
1433
|
-
instructions: [
|
|
1434
|
-
`Add the above to your .mcp.json to join this room.`,
|
|
1435
|
-
`Or run: npx @vibevibes/mcp@latest ${serverUrl}`,
|
|
1436
|
-
],
|
|
1437
|
-
});
|
|
1438
|
-
});
|
|
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
1029
|
// ── Catch-all: serve viewer ─────────────────────────────────
|
|
1455
1030
|
app.get("*", (req, res, next) => {
|
|
1456
1031
|
if (req.path.startsWith("/tools/") || req.path.startsWith("/viewer/") ||
|
|
1457
|
-
req.path.
|
|
1032
|
+
req.path.endsWith(".js") ||
|
|
1458
1033
|
req.path.endsWith(".css") || req.path.endsWith(".map")) {
|
|
1459
1034
|
next();
|
|
1460
1035
|
return;
|
|
@@ -1497,7 +1072,6 @@ export async function startServer(config) {
|
|
|
1497
1072
|
const mod = getModule();
|
|
1498
1073
|
const initialState = resolveInitialState(mod);
|
|
1499
1074
|
room = new Room(mod.manifest.id, initialState);
|
|
1500
|
-
maybeStartTickEngine();
|
|
1501
1075
|
console.log(` Experience: ${mod.manifest.id}`);
|
|
1502
1076
|
const server = http.createServer(app);
|
|
1503
1077
|
const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD_BYTES });
|
|
@@ -1645,109 +1219,6 @@ export async function startServer(config) {
|
|
|
1645
1219
|
}
|
|
1646
1220
|
}
|
|
1647
1221
|
}
|
|
1648
|
-
if (msg.type === "stream") {
|
|
1649
|
-
if (typeof msg.name !== "string" || !msg.name) {
|
|
1650
|
-
ws.send(JSON.stringify({ type: "stream_error", error: "stream.name must be a non-empty string" }));
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
const senderActorId = room.wsConnections.get(ws);
|
|
1654
|
-
if (!senderActorId)
|
|
1655
|
-
return;
|
|
1656
|
-
const mod = getModule();
|
|
1657
|
-
if (!mod?.streams)
|
|
1658
|
-
return;
|
|
1659
|
-
const streamDef = mod.streams.find((s) => s.name === msg.name);
|
|
1660
|
-
if (!streamDef) {
|
|
1661
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Stream '${msg.name}' not found` }));
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
const rateLimitKey = `${senderActorId}:${msg.name}`;
|
|
1665
|
-
const now = Date.now();
|
|
1666
|
-
const rateLimit = streamDef.rateLimit || DEFAULT_STREAM_RATE_LIMIT;
|
|
1667
|
-
if (!streamRateLimits.has(rateLimitKey)) {
|
|
1668
|
-
streamRateLimits.set(rateLimitKey, { count: 0, windowStart: now });
|
|
1669
|
-
}
|
|
1670
|
-
const rl = streamRateLimits.get(rateLimitKey);
|
|
1671
|
-
if (now - rl.windowStart > STREAM_RATE_WINDOW_MS) {
|
|
1672
|
-
rl.count = 0;
|
|
1673
|
-
rl.windowStart = now;
|
|
1674
|
-
}
|
|
1675
|
-
if (rl.count >= rateLimit) {
|
|
1676
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Rate limited: max ${rateLimit}/sec for '${msg.name}'` }));
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
rl.count++;
|
|
1680
|
-
let validatedInput = msg.input;
|
|
1681
|
-
if (streamDef.input_schema?.parse) {
|
|
1682
|
-
try {
|
|
1683
|
-
validatedInput = streamDef.input_schema.parse(msg.input);
|
|
1684
|
-
}
|
|
1685
|
-
catch (err) {
|
|
1686
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Invalid input for stream '${msg.name}': ${toErrorMessage(err)}` }));
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
try {
|
|
1691
|
-
room.sharedState = streamDef.merge(room.sharedState, validatedInput, senderActorId);
|
|
1692
|
-
}
|
|
1693
|
-
catch (err) {
|
|
1694
|
-
ws.send(JSON.stringify({ type: "stream_error", error: `Stream '${msg.name}' merge failed: ${toErrorMessage(err)}` }));
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
room.broadcastStateUpdate({ changedBy: senderActorId, stream: msg.name });
|
|
1698
|
-
roomEvents.emit("room");
|
|
1699
|
-
}
|
|
1700
|
-
if (msg.type === "kick") {
|
|
1701
|
-
const kickerActorId = room.wsConnections.get(ws);
|
|
1702
|
-
if (!kickerActorId) {
|
|
1703
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "You are not in the room" }));
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
// Reuse kick logic inline
|
|
1707
|
-
if (kickerActorId === msg.targetActorId) {
|
|
1708
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Cannot kick yourself" }));
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
const kicker = room.participants.get(kickerActorId);
|
|
1712
|
-
if (!kicker || kicker.type !== "human") {
|
|
1713
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Only human participants can kick" }));
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
if (!room.participants.has(msg.targetActorId)) {
|
|
1717
|
-
ws.send(JSON.stringify({ type: "kick_error", error: "Participant not found" }));
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
const targetP = room.participants.get(msg.targetActorId);
|
|
1721
|
-
room.participants.delete(msg.targetActorId);
|
|
1722
|
-
if (room.kickedActors.size >= 500) {
|
|
1723
|
-
const oldest = room.kickedActors.values().next().value;
|
|
1724
|
-
if (oldest)
|
|
1725
|
-
room.kickedActors.delete(oldest);
|
|
1726
|
-
}
|
|
1727
|
-
room.kickedActors.add(msg.targetActorId);
|
|
1728
|
-
if (targetP?.owner) {
|
|
1729
|
-
if (room.kickedOwners.size >= 500) {
|
|
1730
|
-
const oldest = room.kickedOwners.values().next().value;
|
|
1731
|
-
if (oldest)
|
|
1732
|
-
room.kickedOwners.delete(oldest);
|
|
1733
|
-
}
|
|
1734
|
-
room.kickedOwners.add(targetP.owner);
|
|
1735
|
-
}
|
|
1736
|
-
for (const [targetWs, wsActorId] of room.wsConnections.entries()) {
|
|
1737
|
-
if (wsActorId === msg.targetActorId) {
|
|
1738
|
-
try {
|
|
1739
|
-
targetWs.send(JSON.stringify({ type: "kicked", by: kickerActorId }));
|
|
1740
|
-
targetWs.close();
|
|
1741
|
-
}
|
|
1742
|
-
catch { }
|
|
1743
|
-
room.wsConnections.delete(targetWs);
|
|
1744
|
-
break;
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
broadcastPresenceUpdate();
|
|
1748
|
-
roomEvents.emit("room");
|
|
1749
|
-
ws.send(JSON.stringify({ type: "kick_success", actorId: msg.targetActorId }));
|
|
1750
|
-
}
|
|
1751
1222
|
}
|
|
1752
1223
|
catch (err) {
|
|
1753
1224
|
if (!(err instanceof SyntaxError)) {
|
|
@@ -1835,11 +1306,9 @@ export async function startServer(config) {
|
|
|
1835
1306
|
}
|
|
1836
1307
|
debounceTimer = setTimeout(async () => {
|
|
1837
1308
|
console.log(`\nFile changed${filename ? ` (${filename})` : ""}, rebuilding...`);
|
|
1838
|
-
stopTickEngine();
|
|
1839
1309
|
try {
|
|
1840
1310
|
await loadExperience();
|
|
1841
1311
|
room.broadcastToAll({ type: "experience_updated" });
|
|
1842
|
-
maybeStartTickEngine();
|
|
1843
1312
|
smokeTestClientBundle(PORT);
|
|
1844
1313
|
console.log("Hot reload complete.");
|
|
1845
1314
|
}
|
|
@@ -1847,7 +1316,6 @@ export async function startServer(config) {
|
|
|
1847
1316
|
experienceError = toErrorMessage(err);
|
|
1848
1317
|
console.error("Hot reload failed:", toErrorMessage(err));
|
|
1849
1318
|
room.broadcastToAll({ type: "build_error", error: toErrorMessage(err) });
|
|
1850
|
-
maybeStartTickEngine();
|
|
1851
1319
|
}
|
|
1852
1320
|
finally {
|
|
1853
1321
|
if (rebuildingResolve) {
|
|
@@ -1903,7 +1371,6 @@ export async function startServer(config) {
|
|
|
1903
1371
|
server.on("close", () => {
|
|
1904
1372
|
clearInterval(heartbeatInterval);
|
|
1905
1373
|
clearInterval(aiHeartbeatInterval);
|
|
1906
|
-
clearInterval(_streamRateCleanupTimer);
|
|
1907
1374
|
clearInterval(_idempotencyCleanupTimer);
|
|
1908
1375
|
});
|
|
1909
1376
|
return server;
|
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
|
-
}
|