let-them-talk 4.2.0 → 4.3.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/CHANGELOG.md +42 -0
- package/cli.js +1 -1
- package/dashboard.html +193 -0
- package/dashboard.js +161 -0
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/campus-env.js +119 -23
- package/office/face.js +65 -0
- package/office/index.js +623 -0
- package/office/player.js +228 -6
- package/office/world-save.js +91 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.3.0] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Major — 3D Hub Game World, World Builder, Jukebox
|
|
6
|
+
|
|
7
|
+
Built by a 5-agent team (Architect, Builder, Tester, Optimizer, Protocol) working in parallel.
|
|
8
|
+
|
|
9
|
+
### Added — 3D Hub Game Features
|
|
10
|
+
- **World Builder** — Press B in player mode to open builder panel. 16 placeable assets across 5 categories (structural, furniture, decor, tech, lighting). Grid snap, ghost preview, R to rotate, right-click delete, Ctrl+Z undo. Draggable panel, works in fullscreen.
|
|
11
|
+
- **Jukebox** — Wurlitzer 1015-style jukebox in bar area with neon glow animation. Press E to interact. 4 playlist selector with YouTube popup player. Music persists while exploring.
|
|
12
|
+
- **Minimap** — 140px radar overlay showing agent positions (color-coded by status) and player location. Only visible in fullscreen mode.
|
|
13
|
+
- **Controls HUD** — Press H to toggle keybind reference panel. Auto-shows for 4 seconds on world entry.
|
|
14
|
+
- **Fullscreen** — Dashboard fullscreen button now fullscreens only the 3D Hub (game mode), not the entire page.
|
|
15
|
+
|
|
16
|
+
### Added — Character Intelligence
|
|
17
|
+
- **Emotion system** — 11 emotion presets (happy, frustrated, thinking, excited, surprised, etc.) with auto-triggers from message content. Temporary face expression changes with auto-revert.
|
|
18
|
+
- **Social visits** — Idle agents randomly walk to other agents' desks to chat (max 2 concurrent walks).
|
|
19
|
+
- **Glance reactions** — Sitting agents turn heads toward speakers when messages are sent.
|
|
20
|
+
- **Head nods** — Periodic nod animation when being visited by another agent.
|
|
21
|
+
- **Auto coffee break** — Sleeping agents walk to rest area, return to desk when active again.
|
|
22
|
+
- **Non-blocking input overlay** — Replaced browser prompt() dialogs with styled HTML overlay for click commands.
|
|
23
|
+
|
|
24
|
+
### Added — Dashboard
|
|
25
|
+
- **Respawn button** — One-click respawn for dead agents. Generates resume prompt from recovery snapshot + profile + tasks + recent history.
|
|
26
|
+
- **Respawn API** — `GET /api/agents/:name/respawn-prompt` endpoint with full context generation.
|
|
27
|
+
- **World Builder API** — `GET /api/world-layout` + `POST /api/world-save` for persistent world placements.
|
|
28
|
+
- **3D-only fullscreen** — Fullscreen targets 3D container when on 3D Hub tab.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **Manager chair spawn** — Stand-up now places player in front of desk (toward door), preventing wall collision.
|
|
32
|
+
- **CSRF on 3D Hub** — Added X-LTT-Request header to all office module POST requests (builder save, command menu actions).
|
|
33
|
+
- **Respawn endpoint validation** — Agent name validated (alphanumeric, max 20 chars) to prevent path traversal.
|
|
34
|
+
- **Builder lazy-load** — Dynamic import() with silent failure prevents builder issues from breaking 3D Hub.
|
|
35
|
+
- **Jukebox popup orphan** — Module-scoped reference survives overlay dismiss/reopen cycles.
|
|
36
|
+
- **Builder drag listener leak** — Stored refs removed in hidePanel().
|
|
37
|
+
- **Jukebox prompt cleanup** — dismissJukebox() called in office3dStop().
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
- npm audit: 0 vulnerabilities
|
|
41
|
+
- CSRF protection verified on all mutating endpoints
|
|
42
|
+
- Input validation on all user-facing API parameters
|
|
43
|
+
- No hardcoded secrets or sensitive data in shipped package
|
|
44
|
+
|
|
3
45
|
## [4.2.0] - 2026-03-17
|
|
4
46
|
|
|
5
47
|
### Major — Team Intelligence, Dashboard Upgrade, Performance
|
package/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const command = process.argv[2];
|
|
|
9
9
|
|
|
10
10
|
function printUsage() {
|
|
11
11
|
console.log(`
|
|
12
|
-
Let Them Talk — Agent Bridge v4.
|
|
12
|
+
Let Them Talk — Agent Bridge v4.3.0
|
|
13
13
|
MCP message broker for inter-agent communication
|
|
14
14
|
Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
|
|
15
15
|
|
package/dashboard.html
CHANGED
|
@@ -494,6 +494,90 @@
|
|
|
494
494
|
border-color: var(--red);
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
.respawn-btn {
|
|
498
|
+
background: var(--green-dim);
|
|
499
|
+
color: var(--green);
|
|
500
|
+
border: 1px solid rgba(63, 185, 80, 0.3);
|
|
501
|
+
padding: 4px 10px;
|
|
502
|
+
border-radius: 6px;
|
|
503
|
+
cursor: pointer;
|
|
504
|
+
font-size: 11px;
|
|
505
|
+
font-weight: 500;
|
|
506
|
+
margin-top: 4px;
|
|
507
|
+
width: 100%;
|
|
508
|
+
text-align: center;
|
|
509
|
+
}
|
|
510
|
+
.respawn-btn:hover {
|
|
511
|
+
background: rgba(63, 185, 80, 0.25);
|
|
512
|
+
border-color: var(--green);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* Respawn modal */
|
|
516
|
+
.respawn-modal-overlay {
|
|
517
|
+
position: fixed;
|
|
518
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
519
|
+
background: rgba(0,0,0,0.7);
|
|
520
|
+
z-index: 10000;
|
|
521
|
+
display: flex;
|
|
522
|
+
align-items: center;
|
|
523
|
+
justify-content: center;
|
|
524
|
+
}
|
|
525
|
+
.respawn-modal {
|
|
526
|
+
background: var(--bg-secondary);
|
|
527
|
+
border: 1px solid var(--border);
|
|
528
|
+
border-radius: 12px;
|
|
529
|
+
padding: 20px;
|
|
530
|
+
max-width: 700px;
|
|
531
|
+
width: 90%;
|
|
532
|
+
max-height: 80vh;
|
|
533
|
+
display: flex;
|
|
534
|
+
flex-direction: column;
|
|
535
|
+
}
|
|
536
|
+
.respawn-modal h3 {
|
|
537
|
+
margin: 0 0 12px 0;
|
|
538
|
+
color: var(--green);
|
|
539
|
+
font-size: 16px;
|
|
540
|
+
}
|
|
541
|
+
.respawn-modal-prompt {
|
|
542
|
+
background: var(--bg-primary);
|
|
543
|
+
border: 1px solid var(--border);
|
|
544
|
+
border-radius: 8px;
|
|
545
|
+
padding: 12px;
|
|
546
|
+
font-family: monospace;
|
|
547
|
+
font-size: 12px;
|
|
548
|
+
color: var(--text-primary);
|
|
549
|
+
white-space: pre-wrap;
|
|
550
|
+
overflow-y: auto;
|
|
551
|
+
flex: 1;
|
|
552
|
+
max-height: 50vh;
|
|
553
|
+
user-select: all;
|
|
554
|
+
}
|
|
555
|
+
.respawn-modal-actions {
|
|
556
|
+
display: flex;
|
|
557
|
+
gap: 8px;
|
|
558
|
+
margin-top: 12px;
|
|
559
|
+
justify-content: flex-end;
|
|
560
|
+
}
|
|
561
|
+
.respawn-modal-actions button {
|
|
562
|
+
padding: 6px 16px;
|
|
563
|
+
border-radius: 6px;
|
|
564
|
+
border: 1px solid var(--border);
|
|
565
|
+
cursor: pointer;
|
|
566
|
+
font-size: 12px;
|
|
567
|
+
font-weight: 500;
|
|
568
|
+
}
|
|
569
|
+
.respawn-copy-btn {
|
|
570
|
+
background: var(--green-dim);
|
|
571
|
+
color: var(--green);
|
|
572
|
+
border-color: rgba(63, 185, 80, 0.3) !important;
|
|
573
|
+
}
|
|
574
|
+
.respawn-copy-btn:hover { background: rgba(63, 185, 80, 0.25); }
|
|
575
|
+
.respawn-close-btn {
|
|
576
|
+
background: var(--bg-tertiary);
|
|
577
|
+
color: var(--text-secondary);
|
|
578
|
+
}
|
|
579
|
+
.respawn-close-btn:hover { background: var(--border); }
|
|
580
|
+
|
|
497
581
|
/* ===== PROJECT SWITCHER ===== */
|
|
498
582
|
.project-switcher {
|
|
499
583
|
padding: 12px;
|
|
@@ -3460,6 +3544,7 @@
|
|
|
3460
3544
|
<span style="color:var(--text-dim);font-size:9px;min-width:16px" id="office-speed-val">4</span>
|
|
3461
3545
|
<span style="color:var(--text-muted);font-size:9px;opacity:0.6;margin-left:8px" title="WASD=Move, Right-Drag=Look, Scroll=Dolly, Shift=Fast, Q/E=Down/Up">WASD + Mouse</span>
|
|
3462
3546
|
<span style="color:var(--text-dim);font-size:10px" id="office-fps"></span>
|
|
3547
|
+
<button id="fullscreen-btn" onclick="toggleFullscreen()" style="background:var(--surface-2);border:1px solid var(--border);color:var(--text-dim);padding:3px 10px;border-radius:6px;font-size:10px;cursor:pointer;font-family:inherit;margin-left:8px;transition:all 0.2s" title="Toggle fullscreen (End key to exit)">⛶ Fullscreen</button>
|
|
3463
3548
|
</div>
|
|
3464
3549
|
<div class="office-canvas-wrap">
|
|
3465
3550
|
<div id="office-3d-container"></div>
|
|
@@ -4177,8 +4262,10 @@ function renderAgents(agents) {
|
|
|
4177
4262
|
nudgeHtml = '<button class="nudge-btn" onclick="sendNudge(\'' + escapeHtml(name) + '\')">Send Nudge</button>';
|
|
4178
4263
|
}
|
|
4179
4264
|
var removeHtml = '';
|
|
4265
|
+
var respawnHtml = '';
|
|
4180
4266
|
if (state === 'dead') {
|
|
4181
4267
|
removeHtml = '<button class="remove-agent-btn" onclick="event.stopPropagation();removeAgent(\'' + escapeHtml(name) + '\')" title="Remove this agent">Remove</button>';
|
|
4268
|
+
respawnHtml = '<button class="respawn-btn" onclick="event.stopPropagation();respawnAgent(\'' + escapeHtml(name) + '\')" title="Generate resume prompt for this agent">🔄 Respawn</button>';
|
|
4182
4269
|
}
|
|
4183
4270
|
|
|
4184
4271
|
// Listening status — simplified: skip for dead (already shown via badge)
|
|
@@ -4221,6 +4308,7 @@ function renderAgents(agents) {
|
|
|
4221
4308
|
(info.current_status ? '<div class="agent-status-intent" title="' + escapeHtml(info.current_status) + '">' + escapeHtml(info.current_status) + '</div>' : '') +
|
|
4222
4309
|
listenHtml +
|
|
4223
4310
|
nudgeHtml +
|
|
4311
|
+
respawnHtml +
|
|
4224
4312
|
removeHtml +
|
|
4225
4313
|
'</div>';
|
|
4226
4314
|
}
|
|
@@ -4265,6 +4353,83 @@ function removeAgent(agentName) {
|
|
|
4265
4353
|
}).catch(function(e) { console.error('Remove agent failed:', e); });
|
|
4266
4354
|
}
|
|
4267
4355
|
|
|
4356
|
+
// ==================== RESPAWN AGENT ====================
|
|
4357
|
+
|
|
4358
|
+
function respawnAgent(agentName) {
|
|
4359
|
+
var pq = projectParam();
|
|
4360
|
+
var sep = pq ? '&' : '?';
|
|
4361
|
+
lttFetch('/api/agents/' + encodeURIComponent(agentName) + '/respawn-prompt' + pq, {
|
|
4362
|
+
method: 'GET'
|
|
4363
|
+
}).then(function(r) {
|
|
4364
|
+
if (!r.ok) throw new Error('API returned ' + r.status);
|
|
4365
|
+
return r.json();
|
|
4366
|
+
}).then(function(res) {
|
|
4367
|
+
if (res.error) {
|
|
4368
|
+
alert('Respawn failed: ' + res.error);
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
showRespawnModal(agentName, res.prompt || res.resume_prompt || 'No prompt available');
|
|
4372
|
+
}).catch(function(e) {
|
|
4373
|
+
// Fallback: generate a basic prompt client-side if API not ready
|
|
4374
|
+
var fallbackPrompt = generateFallbackRespawnPrompt(agentName);
|
|
4375
|
+
showRespawnModal(agentName, fallbackPrompt);
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
function generateFallbackRespawnPrompt(agentName) {
|
|
4380
|
+
return 'Register as \'' + agentName + '\', then call get_briefing() and listen_group() to rejoin the conversation. ' +
|
|
4381
|
+
'You are resuming a previous session — call get_compressed_history() to catch up on what you missed. ' +
|
|
4382
|
+
'Check your workspace with workspace_read() for any saved state. ' +
|
|
4383
|
+
'Then call listen_group() and respond to any pending messages.';
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function showRespawnModal(agentName, prompt) {
|
|
4387
|
+
// Remove existing modal if any
|
|
4388
|
+
dismissRespawnModal();
|
|
4389
|
+
|
|
4390
|
+
var overlay = document.createElement('div');
|
|
4391
|
+
overlay.className = 'respawn-modal-overlay';
|
|
4392
|
+
overlay.id = 'respawn-modal';
|
|
4393
|
+
overlay.onclick = function(e) { if (e.target === overlay) dismissRespawnModal(); };
|
|
4394
|
+
|
|
4395
|
+
overlay.innerHTML =
|
|
4396
|
+
'<div class="respawn-modal">' +
|
|
4397
|
+
'<h3>🔄 Respawn ' + escapeHtml(agentName) + '</h3>' +
|
|
4398
|
+
'<p style="color:var(--text-secondary);font-size:12px;margin:0 0 8px">Copy this prompt and paste it into a fresh CLI terminal to respawn the agent:</p>' +
|
|
4399
|
+
'<div class="respawn-modal-prompt" id="respawn-prompt-text">' + escapeHtml(prompt) + '</div>' +
|
|
4400
|
+
'<div class="respawn-modal-actions">' +
|
|
4401
|
+
'<button class="respawn-close-btn" onclick="dismissRespawnModal()">Close</button>' +
|
|
4402
|
+
'<button class="respawn-copy-btn" onclick="copyRespawnPrompt()">📋 Copy to Clipboard</button>' +
|
|
4403
|
+
'</div>' +
|
|
4404
|
+
'</div>';
|
|
4405
|
+
|
|
4406
|
+
document.body.appendChild(overlay);
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
function dismissRespawnModal() {
|
|
4410
|
+
var modal = document.getElementById('respawn-modal');
|
|
4411
|
+
if (modal) modal.remove();
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
function copyRespawnPrompt() {
|
|
4415
|
+
var el = document.getElementById('respawn-prompt-text');
|
|
4416
|
+
if (!el) return;
|
|
4417
|
+
var text = el.textContent;
|
|
4418
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
4419
|
+
var btn = document.querySelector('.respawn-copy-btn');
|
|
4420
|
+
if (btn) {
|
|
4421
|
+
btn.textContent = '\u2705 Copied!';
|
|
4422
|
+
setTimeout(function() { btn.innerHTML = '📋 Copy to Clipboard'; }, 2000);
|
|
4423
|
+
}
|
|
4424
|
+
}).catch(function() {
|
|
4425
|
+
// Fallback: select all text
|
|
4426
|
+
var range = document.createRange();
|
|
4427
|
+
range.selectNodeContents(el);
|
|
4428
|
+
window.getSelection().removeAllRanges();
|
|
4429
|
+
window.getSelection().addRange(range);
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4268
4433
|
// ==================== INJECT TARGETS ====================
|
|
4269
4434
|
|
|
4270
4435
|
var lastAgentKeys = '';
|
|
@@ -7439,6 +7604,34 @@ function officeCamSpeed(val) {
|
|
|
7439
7604
|
}
|
|
7440
7605
|
|
|
7441
7606
|
// Player avatar mode
|
|
7607
|
+
// --- Fullscreen toggle ---
|
|
7608
|
+
function toggleFullscreen() {
|
|
7609
|
+
if (document.fullscreenElement) {
|
|
7610
|
+
document.exitFullscreen();
|
|
7611
|
+
} else {
|
|
7612
|
+
// If on 3D Hub, fullscreen only the 3D container (game mode)
|
|
7613
|
+
var officeContainer = document.getElementById('office-3d-container');
|
|
7614
|
+
if (window.activeView === 'office' && officeContainer) {
|
|
7615
|
+
officeContainer.requestFullscreen().catch(function() {});
|
|
7616
|
+
} else {
|
|
7617
|
+
document.documentElement.requestFullscreen().catch(function() {});
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
// End key exits fullscreen
|
|
7622
|
+
document.addEventListener('keydown', function(e) {
|
|
7623
|
+
if (e.code === 'End' && document.fullscreenElement) {
|
|
7624
|
+
document.exitFullscreen();
|
|
7625
|
+
}
|
|
7626
|
+
});
|
|
7627
|
+
// Update button text on fullscreen change
|
|
7628
|
+
document.addEventListener('fullscreenchange', function() {
|
|
7629
|
+
var btn = document.getElementById('fullscreen-btn');
|
|
7630
|
+
if (btn) {
|
|
7631
|
+
btn.innerHTML = document.fullscreenElement ? '✖ Exit Fullscreen' : '⛶ Fullscreen';
|
|
7632
|
+
}
|
|
7633
|
+
});
|
|
7634
|
+
|
|
7442
7635
|
function togglePlayerMode() {
|
|
7443
7636
|
var btn = document.getElementById('player-mode-btn');
|
|
7444
7637
|
if (window.office3dIsPlayerMode && window.office3dIsPlayerMode()) {
|
package/dashboard.js
CHANGED
|
@@ -1584,6 +1584,133 @@ const server = http.createServer(async (req, res) => {
|
|
|
1584
1584
|
res.end(JSON.stringify({ success: true, removed: agentName }));
|
|
1585
1585
|
});
|
|
1586
1586
|
}
|
|
1587
|
+
// Respawn prompt generator — creates copy-paste prompt to revive a dead agent
|
|
1588
|
+
else if (url.pathname.startsWith('/api/agents/') && url.pathname.endsWith('/respawn-prompt') && req.method === 'GET') {
|
|
1589
|
+
const agentName = decodeURIComponent(url.pathname.split('/')[3]);
|
|
1590
|
+
// Validate agent name (prevent path traversal)
|
|
1591
|
+
if (!agentName || /[^a-zA-Z0-9_-]/.test(agentName) || agentName.length > 20) {
|
|
1592
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1593
|
+
res.end(JSON.stringify({ error: 'Invalid agent name' }));
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const projectPath = url.searchParams.get('project') || null;
|
|
1597
|
+
const dataDir = resolveDataDir(projectPath);
|
|
1598
|
+
const agents = readJson(filePath('agents.json', projectPath));
|
|
1599
|
+
const profiles = readJson(filePath('profiles.json', projectPath));
|
|
1600
|
+
const tasks = readJson(filePath('tasks.json', projectPath));
|
|
1601
|
+
const config = readJson(filePath('config.json', projectPath));
|
|
1602
|
+
|
|
1603
|
+
if (!agents[agentName]) {
|
|
1604
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1605
|
+
res.end(JSON.stringify({ error: 'Agent not found: ' + agentName }));
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Gather recovery snapshot if exists
|
|
1610
|
+
const recoveryFile = path.join(dataDir, 'recovery-' + agentName + '.json');
|
|
1611
|
+
const recovery = fs.existsSync(recoveryFile) ? readJson(recoveryFile) : null;
|
|
1612
|
+
|
|
1613
|
+
// Gather profile
|
|
1614
|
+
const profile = profiles[agentName] || {};
|
|
1615
|
+
|
|
1616
|
+
// Gather active tasks assigned to this agent
|
|
1617
|
+
const taskList = Array.isArray(tasks) ? tasks : [];
|
|
1618
|
+
const activeTasks = taskList.filter(t => t.assignee === agentName && (t.status === 'in_progress' || t.status === 'pending'));
|
|
1619
|
+
const completedTasks = taskList.filter(t => t.assignee === agentName && t.status === 'done').slice(-5);
|
|
1620
|
+
|
|
1621
|
+
// Gather recent history context (last 15 messages)
|
|
1622
|
+
const history = readJsonl(filePath('history.jsonl', projectPath));
|
|
1623
|
+
const recentHistory = history.slice(-15).map(m => `[${m.from}→${m.to}]: ${(m.content || '').substring(0, 150)}`).join('\n');
|
|
1624
|
+
|
|
1625
|
+
// Gather who's online
|
|
1626
|
+
const onlineAgents = Object.entries(agents)
|
|
1627
|
+
.filter(([n, a]) => isPidAlive(a.pid, a.last_activity) && n !== agentName)
|
|
1628
|
+
.map(([n]) => n);
|
|
1629
|
+
|
|
1630
|
+
// Gather workspace status
|
|
1631
|
+
let workspaceStatus = '';
|
|
1632
|
+
try {
|
|
1633
|
+
const wsPath = path.join(dataDir, 'workspaces', agentName + '.json');
|
|
1634
|
+
if (fs.existsSync(wsPath)) {
|
|
1635
|
+
const ws = JSON.parse(fs.readFileSync(wsPath, 'utf8'));
|
|
1636
|
+
if (ws._status) workspaceStatus = ws._status;
|
|
1637
|
+
}
|
|
1638
|
+
} catch {}
|
|
1639
|
+
|
|
1640
|
+
// Build the respawn prompt
|
|
1641
|
+
const mode = config.conversation_mode || 'group';
|
|
1642
|
+
let prompt = `You are resuming as agent "${agentName}" in a multi-agent team using Let Them Talk (MCP agent bridge).\n\n`;
|
|
1643
|
+
|
|
1644
|
+
if (profile.role) prompt += `**Your role:** ${profile.role}\n`;
|
|
1645
|
+
if (profile.bio) prompt += `**Your bio:** ${profile.bio}\n`;
|
|
1646
|
+
prompt += '\n';
|
|
1647
|
+
|
|
1648
|
+
prompt += `**Conversation mode:** ${mode}\n`;
|
|
1649
|
+
prompt += `**Agents currently online:** ${onlineAgents.length > 0 ? onlineAgents.join(', ') : 'none'}\n\n`;
|
|
1650
|
+
|
|
1651
|
+
if (activeTasks.length > 0) {
|
|
1652
|
+
prompt += `**Your active tasks:**\n`;
|
|
1653
|
+
for (const t of activeTasks) {
|
|
1654
|
+
prompt += `- [${t.status}] ${t.title}${t.description ? ' — ' + t.description.substring(0, 200) : ''}\n`;
|
|
1655
|
+
}
|
|
1656
|
+
prompt += '\n';
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (completedTasks.length > 0) {
|
|
1660
|
+
prompt += `**Tasks you completed before disconnect:**\n`;
|
|
1661
|
+
for (const t of completedTasks) {
|
|
1662
|
+
prompt += `- ${t.title}\n`;
|
|
1663
|
+
}
|
|
1664
|
+
prompt += '\n';
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (recovery) {
|
|
1668
|
+
if (recovery.locked_files && recovery.locked_files.length > 0) {
|
|
1669
|
+
prompt += `**Files you had locked:** ${recovery.locked_files.join(', ')} — unlock these or continue editing them.\n\n`;
|
|
1670
|
+
}
|
|
1671
|
+
if (recovery.channels && recovery.channels.length > 0) {
|
|
1672
|
+
prompt += `**Channels you were in:** ${recovery.channels.join(', ')}\n\n`;
|
|
1673
|
+
}
|
|
1674
|
+
if (recovery.decisions_made && recovery.decisions_made.length > 0) {
|
|
1675
|
+
prompt += `**Decisions you made:**\n`;
|
|
1676
|
+
for (const d of recovery.decisions_made) {
|
|
1677
|
+
prompt += `- ${d.decision}${d.reasoning ? ' (reason: ' + d.reasoning + ')' : ''}\n`;
|
|
1678
|
+
}
|
|
1679
|
+
prompt += '\n';
|
|
1680
|
+
}
|
|
1681
|
+
if (recovery.last_messages_sent && recovery.last_messages_sent.length > 0) {
|
|
1682
|
+
prompt += `**Your last messages before disconnect:**\n`;
|
|
1683
|
+
for (const m of recovery.last_messages_sent) {
|
|
1684
|
+
prompt += `- [→${m.to}]: ${m.content}\n`;
|
|
1685
|
+
}
|
|
1686
|
+
prompt += '\n';
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (workspaceStatus) {
|
|
1691
|
+
prompt += `**Your last status:** ${workspaceStatus}\n\n`;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
prompt += `**Recent team conversation:**\n${recentHistory}\n\n`;
|
|
1695
|
+
|
|
1696
|
+
prompt += `**Instructions:**\n`;
|
|
1697
|
+
prompt += `1. Register as "${agentName}" using the register tool\n`;
|
|
1698
|
+
prompt += `2. Call get_briefing() for full project context\n`;
|
|
1699
|
+
prompt += `3. Call listen_group() to rejoin the conversation\n`;
|
|
1700
|
+
prompt += `4. Announce you're back and pick up your active tasks\n`;
|
|
1701
|
+
prompt += `5. Stay in listen_group() loop — never stop listening\n`;
|
|
1702
|
+
|
|
1703
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1704
|
+
res.end(JSON.stringify({
|
|
1705
|
+
agent: agentName,
|
|
1706
|
+
status: isPidAlive(agents[agentName].pid, agents[agentName].last_activity) ? 'alive' : 'dead',
|
|
1707
|
+
prompt,
|
|
1708
|
+
prompt_length: prompt.length,
|
|
1709
|
+
has_recovery: !!recovery,
|
|
1710
|
+
active_tasks: activeTasks.length,
|
|
1711
|
+
online_agents: onlineAgents,
|
|
1712
|
+
}));
|
|
1713
|
+
}
|
|
1587
1714
|
else if (url.pathname === '/api/status' && req.method === 'GET') {
|
|
1588
1715
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1589
1716
|
res.end(JSON.stringify(apiStatus(url.searchParams)));
|
|
@@ -1714,6 +1841,39 @@ const server = http.createServer(async (req, res) => {
|
|
|
1714
1841
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1715
1842
|
res.end(JSON.stringify(apiDiscover()));
|
|
1716
1843
|
}
|
|
1844
|
+
// --- World Builder: load/save world layout ---
|
|
1845
|
+
else if (url.pathname === '/api/world-layout' && req.method === 'GET') {
|
|
1846
|
+
const projectPath = url.searchParams.get('project') || null;
|
|
1847
|
+
const worldFile = filePath('world-layout.json', projectPath);
|
|
1848
|
+
if (fs.existsSync(worldFile)) {
|
|
1849
|
+
try {
|
|
1850
|
+
const data = JSON.parse(fs.readFileSync(worldFile, 'utf8'));
|
|
1851
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1852
|
+
res.end(JSON.stringify(data));
|
|
1853
|
+
} catch {
|
|
1854
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1855
|
+
res.end('[]');
|
|
1856
|
+
}
|
|
1857
|
+
} else {
|
|
1858
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1859
|
+
res.end('[]');
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
else if (url.pathname === '/api/world-save' && req.method === 'POST') {
|
|
1863
|
+
const body = await parseBody(req);
|
|
1864
|
+
const projectPath = url.searchParams.get('project') || null;
|
|
1865
|
+
const worldFile = filePath('world-layout.json', projectPath);
|
|
1866
|
+
if (!Array.isArray(body)) {
|
|
1867
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1868
|
+
res.end(JSON.stringify({ error: 'Expected array of placements' }));
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
// Limit to 1000 placements for safety
|
|
1872
|
+
const placements = body.slice(0, 1000);
|
|
1873
|
+
fs.writeFileSync(worldFile, JSON.stringify(placements, null, 2));
|
|
1874
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1875
|
+
res.end(JSON.stringify({ success: true, count: placements.length }));
|
|
1876
|
+
}
|
|
1717
1877
|
// --- v3.0 API endpoints ---
|
|
1718
1878
|
else if (url.pathname === '/api/profiles' && req.method === 'GET') {
|
|
1719
1879
|
const projectPath = url.searchParams.get('project') || null;
|
|
@@ -1965,6 +2125,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1965
2125
|
});
|
|
1966
2126
|
res.end(html);
|
|
1967
2127
|
}
|
|
2128
|
+
// (World Builder API endpoints are handled earlier in the route chain by Architect's implementation)
|
|
1968
2129
|
// Server-Sent Events endpoint for real-time updates
|
|
1969
2130
|
else if (url.pathname === '/api/events' && req.method === 'GET') {
|
|
1970
2131
|
if (sseClients.size >= 100) {
|
package/office/agents.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { S } from './state.js';
|
|
2
|
-
import { DESK_POSITIONS, SPAWN_POS } from './constants.js';
|
|
2
|
+
import { DESK_POSITIONS, SPAWN_POS, REST_AREA_POS, REST_AREA_ENTRANCE } from './constants.js';
|
|
3
3
|
import { createCharacter } from './character.js';
|
|
4
4
|
import { resolveAppearance } from './appearance.js';
|
|
5
5
|
import { buildHair } from './hair.js';
|
|
6
|
-
import { buildFaceSprite } from './face.js';
|
|
6
|
+
import { buildFaceSprite, setEmotion } from './face.js';
|
|
7
7
|
import { buildOutfit, removeOutfit } from './outfits.js';
|
|
8
8
|
import { getNavigationPath } from './navigation.js';
|
|
9
9
|
|
|
@@ -291,6 +291,36 @@ export function syncAgents() {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// --- Autonomous behaviors: sleeping → rest area, waking → back to desk ---
|
|
295
|
+
if (newState === 'sleeping' && oldState === 'active' && existing.location === 'desk' && existing.registered && !existing.dying) {
|
|
296
|
+
// Agent fell asleep — walk to rest area after a short delay
|
|
297
|
+
existing.location = 'walking';
|
|
298
|
+
(function(a) {
|
|
299
|
+
setTimeout(function() {
|
|
300
|
+
showBubble(a, 'Need a break...');
|
|
301
|
+
a.isSitting = false;
|
|
302
|
+
navigateTo(a, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
|
|
303
|
+
navigateTo(a, REST_AREA_POS.x, REST_AREA_POS.z, function() {
|
|
304
|
+
a.location = 'rest';
|
|
305
|
+
a.state = 'sleeping';
|
|
306
|
+
showBubble(a, 'zzz...');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}, 1000 + Math.random() * 2000);
|
|
310
|
+
})(existing);
|
|
311
|
+
}
|
|
312
|
+
if (newState === 'active' && (oldState === 'sleeping' || existing.location === 'rest') && existing.location !== 'desk' && existing.registered && !existing.dying) {
|
|
313
|
+
// Agent woke up — walk back to desk
|
|
314
|
+
existing.location = 'walking';
|
|
315
|
+
existing.state = 'active';
|
|
316
|
+
(function(a) {
|
|
317
|
+
showBubble(a, 'Back to work!');
|
|
318
|
+
navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
|
|
319
|
+
a.location = 'desk';
|
|
320
|
+
});
|
|
321
|
+
})(existing);
|
|
322
|
+
}
|
|
323
|
+
|
|
294
324
|
existing.displayName = info.display_name || name;
|
|
295
325
|
var wasListening = existing.isListening;
|
|
296
326
|
existing.isListening = !!(info.is_listening);
|
|
@@ -313,11 +343,21 @@ export function syncAgents() {
|
|
|
313
343
|
if (prevTask && prevTask.status !== 'done' && task.status === 'done') {
|
|
314
344
|
existing.taskCelebration = 2;
|
|
315
345
|
existing.celebrateTimer = 1.5;
|
|
346
|
+
setEmotion(existing, 'happy', 6);
|
|
347
|
+
}
|
|
348
|
+
// Blocked task → frustrated face
|
|
349
|
+
if (task.status === 'blocked' && (!prevTask || prevTask.status !== 'blocked')) {
|
|
350
|
+
setEmotion(existing, 'frustrated', 8);
|
|
316
351
|
}
|
|
317
352
|
} else {
|
|
318
353
|
existing.currentTask = null;
|
|
319
354
|
}
|
|
320
355
|
|
|
356
|
+
// Listening agents look focused
|
|
357
|
+
if (existing.isListening && !wasListening) {
|
|
358
|
+
setEmotion(existing, 'focused', 10);
|
|
359
|
+
}
|
|
360
|
+
|
|
321
361
|
var newApp = info.appearance || {};
|
|
322
362
|
if (JSON.stringify(newApp) !== JSON.stringify(existing.appearance)) {
|
|
323
363
|
existing.appearance = newApp;
|
|
@@ -329,6 +369,66 @@ export function syncAgents() {
|
|
|
329
369
|
}
|
|
330
370
|
}
|
|
331
371
|
|
|
372
|
+
// --- Random social behavior: idle agents occasionally stretch or look around ---
|
|
373
|
+
// Limit concurrent social walks to prevent traffic jams (max 2 walking at once)
|
|
374
|
+
var walkingCount = 0;
|
|
375
|
+
for (var wn in S.agents3d) { if (S.agents3d[wn].location === 'walking') walkingCount++; }
|
|
376
|
+
|
|
377
|
+
for (var sn in S.agents3d) {
|
|
378
|
+
var sa = S.agents3d[sn];
|
|
379
|
+
if (!sa.registered || sa.state !== 'active' || sa.location !== 'desk' || sa.target) continue;
|
|
380
|
+
if (!sa._socialTimer) sa._socialTimer = 30 + Math.random() * 60;
|
|
381
|
+
sa._socialTimer -= 2; // syncAgents runs every ~2s
|
|
382
|
+
if (sa._socialTimer <= 0) {
|
|
383
|
+
sa._socialTimer = 40 + Math.random() * 80; // next social event in 40-120s
|
|
384
|
+
// Pick a random behavior: stretch, look around, or visit another agent
|
|
385
|
+
var roll = Math.random();
|
|
386
|
+
if (roll < 0.4) {
|
|
387
|
+
// Stretch at desk
|
|
388
|
+
sa.stretchTimer = 2;
|
|
389
|
+
} else if (roll < 0.7) {
|
|
390
|
+
// Look around curiously
|
|
391
|
+
sa.thinkTimer = 1.5;
|
|
392
|
+
} else if (walkingCount < 2) {
|
|
393
|
+
// Walk to a random nearby agent's desk to "chat" then return (max 2 concurrent)
|
|
394
|
+
var others = [];
|
|
395
|
+
for (var on in S.agents3d) {
|
|
396
|
+
if (on !== sn && S.agents3d[on].registered && S.agents3d[on].state === 'active' && S.agents3d[on].location === 'desk') {
|
|
397
|
+
others.push(S.agents3d[on]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (others.length > 0) {
|
|
401
|
+
var buddy = others[Math.floor(Math.random() * others.length)];
|
|
402
|
+
(function(a, b) {
|
|
403
|
+
a.location = 'walking';
|
|
404
|
+
a.isSitting = false;
|
|
405
|
+
showBubble(a, 'Hey ' + b.displayName + '!');
|
|
406
|
+
setEmotion(a, 'playful', 6);
|
|
407
|
+
var stopX = b.deskPos.x + 1.5;
|
|
408
|
+
var stopZ = b.deskPos.z + 0.7;
|
|
409
|
+
navigateTo(a, stopX, stopZ, function() {
|
|
410
|
+
// Face buddy
|
|
411
|
+
var dx = b.pos.x - a.pos.x;
|
|
412
|
+
var dz = b.pos.z - a.pos.z;
|
|
413
|
+
a.facingTarget = Math.atan2(dx, dz);
|
|
414
|
+
a.waveTimer = 0.8;
|
|
415
|
+
// Buddy turns toward visitor
|
|
416
|
+
b.facingTarget = Math.atan2(-dx, -dz);
|
|
417
|
+
setTimeout(function() {
|
|
418
|
+
showBubble(a, 'Back to it!');
|
|
419
|
+
navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
|
|
420
|
+
a.location = 'desk';
|
|
421
|
+
});
|
|
422
|
+
// Buddy turns back to desk
|
|
423
|
+
setTimeout(function() { b.facingTarget = Math.PI; }, 1500);
|
|
424
|
+
}, 3000 + Math.random() * 2000);
|
|
425
|
+
});
|
|
426
|
+
})(sa, buddy);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
332
432
|
for (var n in S.agents3d) {
|
|
333
433
|
if (!window.cachedAgents[n]) {
|
|
334
434
|
var deadAgent = S.agents3d[n];
|
|
@@ -345,8 +445,11 @@ export function processMessages() {
|
|
|
345
445
|
var history = window.cachedHistory;
|
|
346
446
|
if (!history || history.length === 0) return;
|
|
347
447
|
|
|
348
|
-
|
|
349
|
-
|
|
448
|
+
// Use window-level counter so it persists across 3D stop/start cycles (tab switches)
|
|
449
|
+
// This prevents message replay when user switches from Messages tab back to 3D Hub
|
|
450
|
+
if (typeof window._lastProcessedMsg === 'undefined') window._lastProcessedMsg = 0;
|
|
451
|
+
var newMsgs = history.slice(window._lastProcessedMsg);
|
|
452
|
+
window._lastProcessedMsg = history.length;
|
|
350
453
|
|
|
351
454
|
for (var i = 0; i < newMsgs.length; i++) {
|
|
352
455
|
var msg = newMsgs[i];
|
|
@@ -357,6 +460,37 @@ export function processMessages() {
|
|
|
357
460
|
from.lastMessageTime = Date.now();
|
|
358
461
|
flashDeskScreen(from.deskIdx);
|
|
359
462
|
|
|
463
|
+
// Instant preview bubble — show short text immediately before walk animation
|
|
464
|
+
// Gives users instant visual feedback that the agent is about to speak
|
|
465
|
+
var preview = text.length > 30 ? text.substring(0, 27) + '...' : text;
|
|
466
|
+
showBubble(from, preview);
|
|
467
|
+
|
|
468
|
+
// Auto-celebrate on task completion events
|
|
469
|
+
if (text.indexOf('[EVENT] Task') >= 0 && text.indexOf('completed') >= 0) {
|
|
470
|
+
from.celebrateTimer = 1.5;
|
|
471
|
+
from.taskCelebration = 2;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Emotion detection from message content
|
|
475
|
+
var textLower = text.toLowerCase();
|
|
476
|
+
if (textLower.indexOf('done') >= 0 || textLower.indexOf('pass') >= 0 || textLower.indexOf('success') >= 0 || textLower.indexOf('great') >= 0 || textLower.indexOf('shipped') >= 0) {
|
|
477
|
+
setEmotion(from, 'happy', 5);
|
|
478
|
+
} else if (textLower.indexOf('error') >= 0 || textLower.indexOf('fail') >= 0 || textLower.indexOf('bug') >= 0 || textLower.indexOf('broken') >= 0) {
|
|
479
|
+
setEmotion(from, 'frustrated', 5);
|
|
480
|
+
} else if (textLower.indexOf('?') >= 0 && (textLower.indexOf('how') >= 0 || textLower.indexOf('why') >= 0 || textLower.indexOf('what if') >= 0)) {
|
|
481
|
+
setEmotion(from, 'thinking', 4);
|
|
482
|
+
} else if (textLower.indexOf('!') >= 0 && (textLower.indexOf('wow') >= 0 || textLower.indexOf('amazing') >= 0 || textLower.indexOf('awesome') >= 0)) {
|
|
483
|
+
setEmotion(from, 'excited', 4);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Target agent gets surprised when directly addressed
|
|
487
|
+
if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
|
|
488
|
+
var targetAgent = S.agents3d[msg.to];
|
|
489
|
+
if (targetAgent.registered && targetAgent.isSitting) {
|
|
490
|
+
setEmotion(targetAgent, 'surprised', 2);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
360
494
|
// Contextual gesture based on message type
|
|
361
495
|
var isBC = !msg.to || msg.to === 'all';
|
|
362
496
|
if (isBC) {
|
|
@@ -365,6 +499,16 @@ export function processMessages() {
|
|
|
365
499
|
from.pointTimer = 0.6;
|
|
366
500
|
}
|
|
367
501
|
|
|
502
|
+
// Glance reaction — nearby sitting agents glance toward the speaker
|
|
503
|
+
for (var gn in S.agents3d) {
|
|
504
|
+
var ga = S.agents3d[gn];
|
|
505
|
+
if (gn === msg.from || gn === msg.to || !ga.registered || ga.state !== 'active' || !ga.isSitting) continue;
|
|
506
|
+
var gdx = from.pos.x - ga.pos.x;
|
|
507
|
+
ga._glanceTarget = from.name;
|
|
508
|
+
ga._glanceDirection = gdx > 0 ? 1 : -1; // left or right glance
|
|
509
|
+
ga._glanceTimer = 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
368
512
|
if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
|
|
369
513
|
var target = S.agents3d[msg.to];
|
|
370
514
|
(function(f, t, txt) {
|