@vibevibes/mcp 0.4.1 → 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/index.js +4 -23
- package/dist/server.js +2 -101
- package/hooks/logic.js +3 -24
- package/hooks/stop-hook.js +2 -72
- 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
|
@@ -316,11 +316,9 @@ and returns available tools, current state, participants, and the browser URL.
|
|
|
316
316
|
|
|
317
317
|
Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
318
318
|
url: z.string().optional().describe("Server URL to connect to. Defaults to the MCP server's configured URL."),
|
|
319
|
-
role: z.string().optional().describe("Preferred participant slot role to request
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
metadata: z.record(z.string()).optional().describe("Arbitrary metadata (model, team, tags). Flows to server and viewer. E.g. { model: 'haiku', team: 'blue' }"),
|
|
323
|
-
}, async ({ url, role: requestedRole, subscription, agentMode, metadata }) => {
|
|
319
|
+
role: z.string().optional().describe("Preferred participant slot role to request."),
|
|
320
|
+
metadata: z.record(z.string()).optional().describe("Arbitrary metadata. Flows to server and viewer."),
|
|
321
|
+
}, async ({ url, role: requestedRole, metadata }) => {
|
|
324
322
|
try {
|
|
325
323
|
if (url) {
|
|
326
324
|
SERVER_URL = url.replace(/\/$/, "");
|
|
@@ -381,8 +379,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
381
379
|
const joinBody = { username: id, actorType: "ai", owner: id };
|
|
382
380
|
if (requestedRole)
|
|
383
381
|
joinBody.role = requestedRole;
|
|
384
|
-
if (agentMode)
|
|
385
|
-
joinBody.agentMode = agentMode;
|
|
386
382
|
if (metadata && Object.keys(metadata).length > 0)
|
|
387
383
|
joinBody.metadata = metadata;
|
|
388
384
|
joinData = await fetchJSON("/join", {
|
|
@@ -403,12 +399,6 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
403
399
|
actorId: joinData.actorId,
|
|
404
400
|
role: joinData.role || undefined,
|
|
405
401
|
};
|
|
406
|
-
if (subscription && subscription !== "all") {
|
|
407
|
-
sessionData.subscription = subscription;
|
|
408
|
-
}
|
|
409
|
-
if (agentMode) {
|
|
410
|
-
sessionData.agentMode = agentMode;
|
|
411
|
-
}
|
|
412
402
|
writeFileSync(resolve(AGENTS_DIR, `${id}.json`), JSON.stringify(sessionData, null, 2));
|
|
413
403
|
identity = { actorId: joinData.actorId, owner: id, id };
|
|
414
404
|
currentActorId = joinData.actorId;
|
|
@@ -475,16 +465,7 @@ Call this first, then use act to interact. The stop hook keeps you present.`, {
|
|
|
475
465
|
if (unfilledSlots.length > 0) {
|
|
476
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.")`, ``);
|
|
477
467
|
}
|
|
478
|
-
|
|
479
|
-
if (agentMode === "behavior") {
|
|
480
|
-
outputParts.push(`BEHAVIOR MODE:`, ` You are a behavior-only agent. All actions run autonomously via the tick engine.`, ` No stop hook — you will NOT receive wake-up events.`, ` Set up behaviors now, then disconnect. The tick engine runs them automatically.`, ``);
|
|
481
|
-
}
|
|
482
|
-
else if (agentMode === "manual") {
|
|
483
|
-
outputParts.push(`MANUAL MODE:`, ` You are a manual agent. Use act() for all decisions. No behaviors.`, ` The stop hook keeps you present — use act to interact.`, ` Use act() to interact. The stop hook keeps you present.`, ``);
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
outputParts.push(`FAST BRAIN / SLOW BRAIN:`, ` Fast brain: Register behaviors via _behavior.set for reactive, per-tick actions that run automatically.`, ` Slow brain: Use act() for strategic decisions and adapting to new situations.`, ` Set up your fast brain FIRST, then use your slow brain to observe and adapt.`, ` Use act() to interact. The stop hook keeps you present.`, ``, `You are now a live participant. The stop hook keeps you present — use act to interact.`);
|
|
487
|
-
}
|
|
468
|
+
outputParts.push(`Use act() to interact. The stop hook keeps you present.`);
|
|
488
469
|
// Register the stop hook so Claude Code wakes us on events
|
|
489
470
|
ensureStopHook();
|
|
490
471
|
return { content: [{ type: "text", text: outputParts.filter(Boolean).join("\n") }] };
|
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";
|
|
@@ -195,8 +193,6 @@ class Room {
|
|
|
195
193
|
const detail = {
|
|
196
194
|
actorId, type: p.type, role: p.role, owner: p.owner,
|
|
197
195
|
};
|
|
198
|
-
if (p.agentMode)
|
|
199
|
-
detail.agentMode = p.agentMode;
|
|
200
196
|
if (p.metadata && Object.keys(p.metadata).length > 0)
|
|
201
197
|
detail.metadata = p.metadata;
|
|
202
198
|
return detail;
|
|
@@ -224,7 +220,6 @@ const HISTORY_MAX_LIMIT = 200;
|
|
|
224
220
|
const DEFAULT_STREAM_RATE_LIMIT = 60;
|
|
225
221
|
const STREAM_RATE_WINDOW_MS = 1000;
|
|
226
222
|
const EVENT_BATCH_DEBOUNCE_MS = 50;
|
|
227
|
-
const DEFAULT_TICK_RATE_MS = 50;
|
|
228
223
|
const MAX_BATCH_CALLS = 10;
|
|
229
224
|
const LONG_POLL_MAX_TIMEOUT_MS = 55000;
|
|
230
225
|
const AGENT_CONTEXT_MAX_TIMEOUT_MS = 10000;
|
|
@@ -255,7 +250,6 @@ function defaultObserve(state, _event, _actorId) {
|
|
|
255
250
|
let PORT = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
|
|
256
251
|
let publicUrl = null;
|
|
257
252
|
let room;
|
|
258
|
-
let tickEngine = null;
|
|
259
253
|
let _actorCounter = 0;
|
|
260
254
|
const roomEvents = new EventEmitter();
|
|
261
255
|
roomEvents.setMaxListeners(ROOM_EVENTS_MAX_LISTENERS);
|
|
@@ -347,9 +341,6 @@ function experienceNotLoadedError() {
|
|
|
347
341
|
}
|
|
348
342
|
// ── Experience discovery & loading ──────────────────────────
|
|
349
343
|
function discoverEntryPath() {
|
|
350
|
-
const manifestPath = path.join(PROJECT_ROOT, "manifest.json");
|
|
351
|
-
if (fs.existsSync(manifestPath))
|
|
352
|
-
return manifestPath;
|
|
353
344
|
const tsxPath = path.join(PROJECT_ROOT, "src", "index.tsx");
|
|
354
345
|
if (fs.existsSync(tsxPath))
|
|
355
346
|
return tsxPath;
|
|
@@ -357,15 +348,10 @@ function discoverEntryPath() {
|
|
|
357
348
|
if (fs.existsSync(rootTsx))
|
|
358
349
|
return rootTsx;
|
|
359
350
|
throw new Error(`No experience found in ${PROJECT_ROOT}. ` +
|
|
360
|
-
`Create
|
|
351
|
+
`Create src/index.tsx (TypeScript).`);
|
|
361
352
|
}
|
|
362
|
-
const protocolExecutors = new Map();
|
|
363
353
|
async function loadExperience() {
|
|
364
354
|
const entryPath = discoverEntryPath();
|
|
365
|
-
if (isProtocolExperience(entryPath)) {
|
|
366
|
-
await loadProtocolExperience(entryPath);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
355
|
const [sCode, cCode] = await Promise.all([
|
|
370
356
|
bundleForServer(entryPath),
|
|
371
357
|
bundleForClient(entryPath),
|
|
@@ -387,48 +373,6 @@ async function loadExperience() {
|
|
|
387
373
|
};
|
|
388
374
|
experienceError = null;
|
|
389
375
|
}
|
|
390
|
-
async function loadProtocolExperience(manifestPath) {
|
|
391
|
-
const manifestDir = path.dirname(manifestPath);
|
|
392
|
-
const manifest = loadProtocolManifest(manifestPath);
|
|
393
|
-
const existing = protocolExecutors.get(manifest.id);
|
|
394
|
-
if (existing)
|
|
395
|
-
existing.stop();
|
|
396
|
-
const executor = new SubprocessExecutor(manifest.toolProcess.command, manifest.toolProcess.args || [], manifestDir);
|
|
397
|
-
executor.start();
|
|
398
|
-
protocolExecutors.set(manifest.id, executor);
|
|
399
|
-
try {
|
|
400
|
-
await executor.send("init", { experienceId: manifest.id }, 5000);
|
|
401
|
-
}
|
|
402
|
-
catch { }
|
|
403
|
-
const mod = createProtocolModule(manifest, executor, manifestDir);
|
|
404
|
-
loadedExperience = {
|
|
405
|
-
module: mod,
|
|
406
|
-
clientBundle: "",
|
|
407
|
-
serverCode: JSON.stringify(manifest),
|
|
408
|
-
loadedAt: Date.now(),
|
|
409
|
-
sourcePath: manifestPath,
|
|
410
|
-
};
|
|
411
|
-
experienceError = null;
|
|
412
|
-
console.log(`[protocol] Loaded ${manifest.title} (${manifest.id}) — ${manifest.tools.length} tools`);
|
|
413
|
-
}
|
|
414
|
-
// ── Tick Engine lifecycle ──────────────────────────────────
|
|
415
|
-
function maybeStartTickEngine() {
|
|
416
|
-
if (!loadedExperience)
|
|
417
|
-
return;
|
|
418
|
-
const manifest = loadedExperience.module?.manifest;
|
|
419
|
-
if (manifest?.netcode !== "tick")
|
|
420
|
-
return;
|
|
421
|
-
stopTickEngine();
|
|
422
|
-
const tickRateMs = manifest.tickRateMs || DEFAULT_TICK_RATE_MS;
|
|
423
|
-
tickEngine = new TickEngine(room, loadedExperience, roomEvents, tickRateMs);
|
|
424
|
-
tickEngine.start();
|
|
425
|
-
}
|
|
426
|
-
function stopTickEngine() {
|
|
427
|
-
if (tickEngine) {
|
|
428
|
-
tickEngine.stop();
|
|
429
|
-
tickEngine = null;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
376
|
// ── Express app ────────────────────────────────────────────
|
|
433
377
|
const app = express();
|
|
434
378
|
app.use(express.json({ limit: JSON_BODY_LIMIT }));
|
|
@@ -581,11 +525,9 @@ app.post("/join", (req, res) => {
|
|
|
581
525
|
res.status(500).json({ error: experienceNotLoadedError() });
|
|
582
526
|
return;
|
|
583
527
|
}
|
|
584
|
-
const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole,
|
|
528
|
+
const { username = "user", actorType: rawActorType = "human", owner, role: requestedRole, metadata: rawMetadata } = req.body;
|
|
585
529
|
const actorType = rawActorType === "ai" ? "ai" : "human";
|
|
586
530
|
const resolvedOwner = owner || username;
|
|
587
|
-
const VALID_AGENT_MODES = ["behavior", "manual", "hybrid"];
|
|
588
|
-
const agentMode = actorType === "ai" && typeof rawAgentMode === "string" && VALID_AGENT_MODES.includes(rawAgentMode) ? rawAgentMode : undefined;
|
|
589
531
|
let metadata;
|
|
590
532
|
if (rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
|
|
591
533
|
metadata = {};
|
|
@@ -719,8 +661,6 @@ app.post("/join", (req, res) => {
|
|
|
719
661
|
participant.systemPrompt = slotSystemPrompt;
|
|
720
662
|
if (!slotRole && requestedRole)
|
|
721
663
|
participant.role = requestedRole;
|
|
722
|
-
if (agentMode)
|
|
723
|
-
participant.agentMode = agentMode;
|
|
724
664
|
if (metadata)
|
|
725
665
|
participant.metadata = metadata;
|
|
726
666
|
room.participants.set(actorId, participant);
|
|
@@ -925,11 +865,6 @@ async function executeTool(toolName, actorId, input = {}, owner, expiredFlag) {
|
|
|
925
865
|
observation,
|
|
926
866
|
});
|
|
927
867
|
roomEvents.emit("room");
|
|
928
|
-
const baseTool = toolName.includes(':') ? toolName.split(':').pop() : toolName;
|
|
929
|
-
if (baseTool.startsWith("_behavior.")) {
|
|
930
|
-
if (tickEngine)
|
|
931
|
-
tickEngine.markDirty();
|
|
932
|
-
}
|
|
933
868
|
return { tool: toolName, output, observation };
|
|
934
869
|
}
|
|
935
870
|
// ── Single tool HTTP endpoint ───────────────────────────────
|
|
@@ -1197,8 +1132,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1197
1132
|
return room.events.filter(e => {
|
|
1198
1133
|
if (requestingOwner && e.owner === requestingOwner)
|
|
1199
1134
|
return false;
|
|
1200
|
-
if (e.actorId === "_tick-engine" || e.owner === "_system")
|
|
1201
|
-
return false;
|
|
1202
1135
|
return e.ts > since;
|
|
1203
1136
|
}).sort((a, b) => a.ts - b.ts);
|
|
1204
1137
|
};
|
|
@@ -1250,7 +1183,6 @@ app.get("/agent-context", (req, res) => {
|
|
|
1250
1183
|
lastError,
|
|
1251
1184
|
browserErrors: recentBrowserErrors.length > 0 ? recentBrowserErrors : undefined,
|
|
1252
1185
|
participants: room.participantList(),
|
|
1253
|
-
tickEngine: tickEngine ? { enabled: true, tickCount: tickEngine.getStatus().tickCount } : undefined,
|
|
1254
1186
|
eventCursor,
|
|
1255
1187
|
};
|
|
1256
1188
|
};
|
|
@@ -1394,10 +1326,8 @@ app.post("/reset", (_req, res) => {
|
|
|
1394
1326
|
// ── Sync (re-bundle) ──────────────────────────────────────
|
|
1395
1327
|
app.post("/sync", async (_req, res) => {
|
|
1396
1328
|
try {
|
|
1397
|
-
stopTickEngine();
|
|
1398
1329
|
await loadExperience();
|
|
1399
1330
|
room.broadcastToAll({ type: "experience_updated" });
|
|
1400
|
-
maybeStartTickEngine();
|
|
1401
1331
|
const mod = getModule();
|
|
1402
1332
|
res.json({ synced: true, title: mod?.manifest?.title });
|
|
1403
1333
|
}
|
|
@@ -1422,14 +1352,6 @@ app.get("/experiences", async (_req, res) => {
|
|
|
1422
1352
|
tags: mod.manifest.tags,
|
|
1423
1353
|
}]);
|
|
1424
1354
|
});
|
|
1425
|
-
// ── Tick status ────────────────────────────────────────────
|
|
1426
|
-
app.get("/tick-status", (_req, res) => {
|
|
1427
|
-
if (!tickEngine) {
|
|
1428
|
-
res.json({ enabled: false, tickRateMs: 0, tickCount: 0, behaviorsTotal: 0, behaviorsEnabled: 0, lastTick: null });
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
res.json(tickEngine.getStatus());
|
|
1432
|
-
});
|
|
1433
1355
|
// ── MCP config ─────────────────────────────────────────────
|
|
1434
1356
|
app.get("/mcp-config", (_req, res) => {
|
|
1435
1357
|
const serverUrl = getBaseUrl();
|
|
@@ -1447,21 +1369,6 @@ app.get("/mcp-config", (_req, res) => {
|
|
|
1447
1369
|
],
|
|
1448
1370
|
});
|
|
1449
1371
|
});
|
|
1450
|
-
// ── Protocol experience HTML canvas ───────────────────────
|
|
1451
|
-
app.get("/canvas", async (_req, res) => {
|
|
1452
|
-
if (!loadedExperience) {
|
|
1453
|
-
res.status(500).json({ error: experienceNotLoadedError() });
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
const canvasPath = loadedExperience.module?._canvasPath;
|
|
1457
|
-
if (!canvasPath) {
|
|
1458
|
-
res.status(404).json({ error: "This experience has no HTML canvas" });
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
res.setHeader("Content-Type", "text/html");
|
|
1462
|
-
setNoCacheHeaders(res);
|
|
1463
|
-
res.sendFile(canvasPath);
|
|
1464
|
-
});
|
|
1465
1372
|
// ── Catch-all: serve viewer ─────────────────────────────────
|
|
1466
1373
|
app.get("*", (req, res, next) => {
|
|
1467
1374
|
if (req.path.startsWith("/tools/") || req.path.startsWith("/viewer/") ||
|
|
@@ -1508,7 +1415,6 @@ export async function startServer(config) {
|
|
|
1508
1415
|
const mod = getModule();
|
|
1509
1416
|
const initialState = resolveInitialState(mod);
|
|
1510
1417
|
room = new Room(mod.manifest.id, initialState);
|
|
1511
|
-
maybeStartTickEngine();
|
|
1512
1418
|
console.log(` Experience: ${mod.manifest.id}`);
|
|
1513
1419
|
const server = http.createServer(app);
|
|
1514
1420
|
const wss = new WebSocketServer({ server, maxPayload: WS_MAX_PAYLOAD_BYTES });
|
|
@@ -1819,8 +1725,6 @@ export async function startServer(config) {
|
|
|
1819
1725
|
for (const [actorId, p] of room.participants) {
|
|
1820
1726
|
if (p.type !== "ai")
|
|
1821
1727
|
continue;
|
|
1822
|
-
if (p.agentMode === "behavior")
|
|
1823
|
-
continue;
|
|
1824
1728
|
const lastSeen = p.lastPollAt || p.joinedAt;
|
|
1825
1729
|
if (now - lastSeen > AI_HEARTBEAT_TIMEOUT_MS)
|
|
1826
1730
|
toEvict.push(actorId);
|
|
@@ -1848,11 +1752,9 @@ export async function startServer(config) {
|
|
|
1848
1752
|
}
|
|
1849
1753
|
debounceTimer = setTimeout(async () => {
|
|
1850
1754
|
console.log(`\nFile changed${filename ? ` (${filename})` : ""}, rebuilding...`);
|
|
1851
|
-
stopTickEngine();
|
|
1852
1755
|
try {
|
|
1853
1756
|
await loadExperience();
|
|
1854
1757
|
room.broadcastToAll({ type: "experience_updated" });
|
|
1855
|
-
maybeStartTickEngine();
|
|
1856
1758
|
smokeTestClientBundle(PORT);
|
|
1857
1759
|
console.log("Hot reload complete.");
|
|
1858
1760
|
}
|
|
@@ -1860,7 +1762,6 @@ export async function startServer(config) {
|
|
|
1860
1762
|
experienceError = toErrorMessage(err);
|
|
1861
1763
|
console.error("Hot reload failed:", toErrorMessage(err));
|
|
1862
1764
|
room.broadcastToAll({ type: "build_error", error: toErrorMessage(err) });
|
|
1863
|
-
maybeStartTickEngine();
|
|
1864
1765
|
}
|
|
1865
1766
|
finally {
|
|
1866
1767
|
if (rebuildingResolve) {
|
package/hooks/logic.js
CHANGED
|
@@ -92,10 +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
|
-
|
|
96
|
-
const visibleEvents = ctx.events.filter((e) => !(e.tool || "").startsWith("_behavior.") &&
|
|
97
|
-
e.actorId !== "_tick-engine" &&
|
|
98
|
-
e.owner !== "_system");
|
|
95
|
+
const visibleEvents = ctx.events;
|
|
99
96
|
if (visibleEvents.length > 0) {
|
|
100
97
|
for (const e of visibleEvents) {
|
|
101
98
|
const actor = e.role || e.owner || (e.actorId ? e.actorId.split("-")[0] : "?");
|
|
@@ -115,16 +112,6 @@ export function formatPrompt(ctx) {
|
|
|
115
112
|
parts.push(` ${roomId} (${info.experience}, ${p} participant${p !== 1 ? "s" : ""})`);
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
|
-
// Tick engine status — only show if something interesting
|
|
119
|
-
if (ctx.tickEngines) {
|
|
120
|
-
const activeEngines = Object.entries(ctx.tickEngines).filter(([, status]) => status.enabled && status.behaviorsActive > 0);
|
|
121
|
-
if (activeEngines.length > 0) {
|
|
122
|
-
parts.push("");
|
|
123
|
-
for (const [roomId, status] of activeEngines) {
|
|
124
|
-
parts.push(`Tick engine [${roomId}]: ${status.behaviorsActive} behavior(s) active, ${status.tickCount} ticks`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
115
|
// Participants (deduplicated)
|
|
129
116
|
if (ctx.participants && ctx.participants.length > 0) {
|
|
130
117
|
const unique = [...new Set(ctx.participants)];
|
|
@@ -139,18 +126,10 @@ export function formatPrompt(ctx) {
|
|
|
139
126
|
* Returns null to allow exit (no state file = not in agent mode).
|
|
140
127
|
* Returns a StopDecision to block exit and feed context to Claude.
|
|
141
128
|
*/
|
|
142
|
-
export function makeDecision(ctx, iteration
|
|
143
|
-
// No state file — allow normal exit
|
|
129
|
+
export function makeDecision(ctx, iteration) {
|
|
144
130
|
if (ctx === null)
|
|
145
131
|
return null;
|
|
146
|
-
|
|
147
|
-
if (agentState?.agentMode === "behavior")
|
|
148
|
-
return null;
|
|
149
|
-
// Only count non-system events (tick engine events are noise)
|
|
150
|
-
const realEvents = ctx.events?.filter((e) => !(e.tool || "").startsWith("_behavior.") &&
|
|
151
|
-
e.actorId !== "_tick-engine" &&
|
|
152
|
-
e.owner !== "_system") || [];
|
|
153
|
-
const hasEvents = realEvents.length > 0;
|
|
132
|
+
const hasEvents = (ctx.events?.length || 0) > 0;
|
|
154
133
|
const hasError = !!ctx.lastError;
|
|
155
134
|
const hasBrowserErrors = ctx.browserErrors != null && ctx.browserErrors.length > 0;
|
|
156
135
|
const hasObserveError = !!ctx.observeError;
|
package/hooks/stop-hook.js
CHANGED
|
@@ -165,13 +165,7 @@ async function main() {
|
|
|
165
165
|
}
|
|
166
166
|
// In-memory cursor per agent (not written to disk)
|
|
167
167
|
const cursors = new Map(); // actorId → lastEventCursor
|
|
168
|
-
//
|
|
169
|
-
const lastPhase = new Map(); // owner → last phase value
|
|
170
|
-
// Build the list of OUR agents from agent files (not all AI agents on server).
|
|
171
|
-
// Each Claude Code workspace only polls its own agents — prevents cross-terminal
|
|
172
|
-
// observation merging that causes identity confusion (wrong myClaimedHero, wrong role).
|
|
173
|
-
// Reuse the already-loaded agents array — reconciliation may have deleted stale files,
|
|
174
|
-
// so filter to only those whose files still exist on disk.
|
|
168
|
+
// Build the list of OUR agents from agent files.
|
|
175
169
|
const ourAgents = agents
|
|
176
170
|
.filter(a => existsSync(resolve(AGENTS_DIR, a._filename || `${a.owner}.json`)))
|
|
177
171
|
.map((a) => ({
|
|
@@ -182,16 +176,7 @@ async function main() {
|
|
|
182
176
|
_filename: a._filename,
|
|
183
177
|
serverUrl: a.serverUrl || session.serverUrl,
|
|
184
178
|
roomId: a.roomId || undefined,
|
|
185
|
-
subscription: a.subscription || "all",
|
|
186
|
-
agentMode: a.agentMode || undefined,
|
|
187
|
-
lastPhase: a.lastPhase,
|
|
188
179
|
}));
|
|
189
|
-
// Initialize lastPhase from persisted agent files (survives across invocations)
|
|
190
|
-
for (const agent of ourAgents) {
|
|
191
|
-
if (agent.subscription === "phase" && agent.lastPhase !== undefined) {
|
|
192
|
-
lastPhase.set(agent.owner, agent.lastPhase ?? null);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
180
|
if (ourAgents.length === 0) {
|
|
196
181
|
cleanupAndExit(agents.map(a => a._filename || `${a.owner}.json`));
|
|
197
182
|
return;
|
|
@@ -242,68 +227,13 @@ async function main() {
|
|
|
242
227
|
r.ctx.lastError ||
|
|
243
228
|
r.ctx.observeError ||
|
|
244
229
|
(r.ctx.browserErrors != null && r.ctx.browserErrors.length > 0)));
|
|
245
|
-
// Filter out behavior-only agents — they run autonomously via tick engine, never need waking
|
|
246
|
-
live = live.filter((r) => r.agent.agentMode !== "behavior");
|
|
247
|
-
// Apply subscription filtering: "phase" agents only wake on phase changes or errors
|
|
248
|
-
live = live.filter((r) => {
|
|
249
|
-
if (r.agent.subscription !== "phase")
|
|
250
|
-
return true; // "all" passes through
|
|
251
|
-
// Always extract and persist current phase (even on error wakeups)
|
|
252
|
-
const currentPhase = typeof r.ctx?.observation?.phase === "string"
|
|
253
|
-
? r.ctx.observation.phase : null;
|
|
254
|
-
const prevPhase = lastPhase.get(r.agent.owner) ?? null;
|
|
255
|
-
lastPhase.set(r.agent.owner, currentPhase);
|
|
256
|
-
// Always wake on errors
|
|
257
|
-
if (r.ctx?.lastError || r.ctx?.observeError ||
|
|
258
|
-
(r.ctx?.browserErrors != null && r.ctx.browserErrors.length > 0))
|
|
259
|
-
return true;
|
|
260
|
-
// Phase changed — wake
|
|
261
|
-
if (currentPhase !== prevPhase)
|
|
262
|
-
return true;
|
|
263
|
-
// Check for phase-related events from agent's own room only
|
|
264
|
-
const phaseEvents = r.ctx?.events?.filter((e) => {
|
|
265
|
-
if (e.roomId && r.agent.roomId && e.roomId !== r.agent.roomId)
|
|
266
|
-
return false;
|
|
267
|
-
return /phase|start|reset|end|finish/i.test(e.tool || "");
|
|
268
|
-
}) || [];
|
|
269
|
-
if (phaseEvents.length > 0)
|
|
270
|
-
return true;
|
|
271
|
-
return false; // Not a phase change — skip
|
|
272
|
-
});
|
|
273
|
-
// Persist lastPhase for phase-subscription agents across invocations.
|
|
274
|
-
// Update agent.lastPhase in memory (bumpIteration writes it for woken agents).
|
|
275
|
-
// For filtered-out agents, write to disk here so next invocation sees the correct phase.
|
|
276
|
-
const liveOwners = new Set(live.map((r) => r.agent.owner));
|
|
277
|
-
for (const { agent } of results) {
|
|
278
|
-
if (agent.subscription !== "phase")
|
|
279
|
-
continue;
|
|
280
|
-
if (!lastPhase.has(agent.owner))
|
|
281
|
-
continue;
|
|
282
|
-
const phaseVal = lastPhase.get(agent.owner) ?? null;
|
|
283
|
-
agent.lastPhase = phaseVal;
|
|
284
|
-
if (!liveOwners.has(agent.owner)) {
|
|
285
|
-
try {
|
|
286
|
-
const filePath = resolve(AGENTS_DIR, agent._filename || `${agent.owner}.json`);
|
|
287
|
-
const data = { ...agent };
|
|
288
|
-
delete data._filename;
|
|
289
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
290
|
-
}
|
|
291
|
-
catch { /* Non-fatal — best-effort persistence */ }
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
230
|
if (live.length > 0) {
|
|
295
231
|
if (live.length > 1) {
|
|
296
232
|
process.stderr.write(`[vibevibes] Warning: ${live.length} agent files found but expected 1. Using first agent only.\n`);
|
|
297
233
|
}
|
|
298
234
|
const { agent, ctx } = live[0];
|
|
299
235
|
const iteration = doneOwners.has(agent.owner) ? (agent.iteration || 0) + 1 : bumpIteration(agent);
|
|
300
|
-
const
|
|
301
|
-
roomId: agent.roomId || session.roomId,
|
|
302
|
-
role: agent.role,
|
|
303
|
-
iteration,
|
|
304
|
-
agentMode: agent.agentMode,
|
|
305
|
-
};
|
|
306
|
-
const decision = makeDecision(ctx, iteration, agentState);
|
|
236
|
+
const decision = makeDecision(ctx, iteration);
|
|
307
237
|
if (decision) {
|
|
308
238
|
process.stdout.write(JSON.stringify(decision));
|
|
309
239
|
}
|
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
|
-
}
|