let-them-talk 3.6.0 → 3.6.2
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 +22 -0
- package/cli.js +1 -1
- package/dashboard.html +10 -0
- package/office/agents.js +89 -15
- package/office/animation.js +34 -0
- package/office/campus-env.js +1461 -0
- package/office/environment.js +10 -0
- package/office/index.js +52 -16
- package/office/navigation.js +263 -0
- package/office/scene.js +3 -3
- package/office/spectator-camera.js +1 -1
- package/office/state.js +1 -1
- package/package.json +1 -1
- package/server.js +63 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.6.2] - 2026-03-16
|
|
4
|
+
|
|
5
|
+
### Added — Message Awareness System
|
|
6
|
+
- **Sender gets busy status** — `send_message` and `broadcast` tell you when recipients are working (not listening) so you know messages are queued
|
|
7
|
+
- **Pending message nudge** — every non-listen tool call checks for unread messages and tells the agent to call `listen_group()` soon
|
|
8
|
+
- **Message age tracking** — `listen_group` shows `age_seconds` per message and `delayed: true` flag for messages older than 30s
|
|
9
|
+
- **Agent status in batch** — `listen_group` returns `agents_status` map showing who is `listening` vs `working`
|
|
10
|
+
- **listen_group retry** — timeout now returns `retry: true` with explicit instruction to call again immediately
|
|
11
|
+
- **next_action field** — successful `listen_group` response tells agent to call `listen_group()` again after responding
|
|
12
|
+
- **Ctrl key removed from camera** — no longer moves camera down (Q/E only)
|
|
13
|
+
|
|
14
|
+
### Added — 3D World: Campus Environment & Navigation
|
|
15
|
+
- **Campus environment** — new outdoor environment option with buildings, paths, green spaces
|
|
16
|
+
- **Navigation system** — pathfinding for agents to walk around obstacles instead of through walls
|
|
17
|
+
- **Door animations** — manager office door slides open when agents approach, closes when they leave
|
|
18
|
+
- **Roof visibility** — roof hides when camera is above ceiling height
|
|
19
|
+
|
|
20
|
+
## [3.6.1] - 2026-03-16
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **3D Hub black screen on page load** — the office module loads asynchronously, but the initial `switchView('office')` fired before `office3dStart` was defined. Added auto-start at end of module so the 3D Hub loads immediately on refresh.
|
|
24
|
+
|
|
3
25
|
## [3.6.0] - 2026-03-16
|
|
4
26
|
|
|
5
27
|
### Added — Managed Conversation Mode
|
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 v3.6.
|
|
12
|
+
Let Them Talk — Agent Bridge v3.6.2
|
|
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
|
@@ -2558,6 +2558,15 @@
|
|
|
2558
2558
|
opacity: 0;
|
|
2559
2559
|
transition: none;
|
|
2560
2560
|
}
|
|
2561
|
+
.office3d-listen-lost {
|
|
2562
|
+
pointer-events: none;
|
|
2563
|
+
font-family: Inter, sans-serif;
|
|
2564
|
+
white-space: nowrap;
|
|
2565
|
+
}
|
|
2566
|
+
@keyframes office3d-pulse {
|
|
2567
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
2568
|
+
50% { opacity: 0.5; transform: scale(1.1); }
|
|
2569
|
+
}
|
|
2561
2570
|
|
|
2562
2571
|
/* 3D task indicator */
|
|
2563
2572
|
.office3d-task-indicator {
|
|
@@ -3300,6 +3309,7 @@
|
|
|
3300
3309
|
<div class="office-toolbar">
|
|
3301
3310
|
<label>Environment:</label>
|
|
3302
3311
|
<select id="office-env-select" onchange="officeSetEnvironment(this.value)">
|
|
3312
|
+
<option value="campus" selected>Tech Campus</option>
|
|
3303
3313
|
<option value="modern">Modern Office</option>
|
|
3304
3314
|
<option value="startup">Startup Garage</option>
|
|
3305
3315
|
</select>
|
package/office/agents.js
CHANGED
|
@@ -5,6 +5,34 @@ import { resolveAppearance } from './appearance.js';
|
|
|
5
5
|
import { buildHair } from './hair.js';
|
|
6
6
|
import { buildFaceSprite } from './face.js';
|
|
7
7
|
import { buildOutfit, removeOutfit } from './outfits.js';
|
|
8
|
+
import { getNavigationPath } from './navigation.js';
|
|
9
|
+
|
|
10
|
+
// Navigate agent using waypoint pathfinding (campus) or direct walk (other envs)
|
|
11
|
+
export function navigateTo(agent, tx, tz, callback) {
|
|
12
|
+
var path = getNavigationPath(agent.pos.x, agent.pos.z, tx, tz);
|
|
13
|
+
if (!path || path.length === 0) {
|
|
14
|
+
walkTo(agent, tx, tz, callback);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Queue all waypoints, put callback on the last one
|
|
18
|
+
agent.walkQueue = [];
|
|
19
|
+
for (var i = 1; i < path.length; i++) {
|
|
20
|
+
agent.walkQueue.push({ x: path[i].x, z: path[i].z, cb: null, triggerDoor: path[i].triggerDoor });
|
|
21
|
+
}
|
|
22
|
+
// Attach callback to last queued point (or first walk if only 1 point)
|
|
23
|
+
if (agent.walkQueue.length > 0) {
|
|
24
|
+
agent.walkQueue[agent.walkQueue.length - 1].cb = callback;
|
|
25
|
+
}
|
|
26
|
+
// Start walking to first point
|
|
27
|
+
var first = path[0];
|
|
28
|
+
walkTo(agent, first.x, first.z, first.triggerDoor ? function() { triggerManagerDoor(true); } : (path.length === 1 ? callback : null));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function triggerManagerDoor(open) {
|
|
32
|
+
if (S._managerDoor) {
|
|
33
|
+
S._managerDoorOpen = open ? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
8
36
|
|
|
9
37
|
export function walkTo(agent, tx, tz, callback) {
|
|
10
38
|
var dx = tx - agent.pos.x;
|
|
@@ -25,13 +53,39 @@ export function showBubble(agent, text) {
|
|
|
25
53
|
agent.bubbleText = display;
|
|
26
54
|
}
|
|
27
55
|
|
|
56
|
+
function getDeskPositions() {
|
|
57
|
+
return S._campusDeskPositions || DESK_POSITIONS;
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
function assignDesk(agentName) {
|
|
61
|
+
var desks = getDeskPositions();
|
|
29
62
|
var used = {};
|
|
30
63
|
for (var n in S.agents3d) used[S.agents3d[n].deskIdx] = true;
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
|
|
65
|
+
// If campus mode, check if agent has "Manager" role or name — assign last desk (manager office)
|
|
66
|
+
if (S.currentEnv === 'campus' && desks.length > 0) {
|
|
67
|
+
var info = (window.cachedAgents || {})[agentName] || {};
|
|
68
|
+
var role = (info.role || '').toLowerCase();
|
|
69
|
+
var dname = (info.display_name || agentName).toLowerCase();
|
|
70
|
+
var regName = agentName.toLowerCase();
|
|
71
|
+
var isManager = role === 'manager' || role === 'project lead' || role === 'ceo' || role === 'director' ||
|
|
72
|
+
role.indexOf('project manager') >= 0 || role.indexOf('team lead') >= 0 ||
|
|
73
|
+
dname === 'manager' || regName === 'manager';
|
|
74
|
+
var managerIdx = desks.length - 1; // last desk is manager office
|
|
75
|
+
if (isManager && !used[managerIdx]) {
|
|
76
|
+
return managerIdx;
|
|
77
|
+
}
|
|
78
|
+
// Non-manager agents skip the manager desk
|
|
79
|
+
for (var i = 0; i < desks.length - 1; i++) {
|
|
80
|
+
if (!used[i]) return i;
|
|
81
|
+
}
|
|
82
|
+
return Object.keys(S.agents3d).length % (desks.length - 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (var j = 0; j < desks.length; j++) {
|
|
86
|
+
if (!used[j]) return j;
|
|
33
87
|
}
|
|
34
|
-
return Object.keys(S.agents3d).length %
|
|
88
|
+
return Object.keys(S.agents3d).length % desks.length;
|
|
35
89
|
}
|
|
36
90
|
|
|
37
91
|
function fetchTasks() {
|
|
@@ -75,13 +129,19 @@ function updateLabel(agent) {
|
|
|
75
129
|
}
|
|
76
130
|
}
|
|
77
131
|
|
|
78
|
-
function updateDeskScreen(deskIdx, status) {
|
|
132
|
+
function updateDeskScreen(deskIdx, status, isListening) {
|
|
79
133
|
var desk = S.deskMeshes[deskIdx];
|
|
80
134
|
if (!desk) return;
|
|
81
|
-
if (status === 'active') {
|
|
82
|
-
|
|
135
|
+
if (status === 'active' && isListening) {
|
|
136
|
+
// Listening — green screen
|
|
137
|
+
desk.screenMat.emissive.setHex(0x22c55e);
|
|
83
138
|
desk.screenMat.emissiveIntensity = 0.5;
|
|
84
|
-
desk.screenMat.color.setHex(
|
|
139
|
+
desk.screenMat.color.setHex(0x22c55e);
|
|
140
|
+
} else if (status === 'active' && !isListening) {
|
|
141
|
+
// Active but NOT listening — red screen
|
|
142
|
+
desk.screenMat.emissive.setHex(0xef4444);
|
|
143
|
+
desk.screenMat.emissiveIntensity = 0.6;
|
|
144
|
+
desk.screenMat.color.setHex(0xef4444);
|
|
85
145
|
} else if (status === 'sleeping') {
|
|
86
146
|
desk.screenMat.emissive.setHex(0x1a2744);
|
|
87
147
|
desk.screenMat.emissiveIntensity = 0.15;
|
|
@@ -151,7 +211,8 @@ export function syncAgents() {
|
|
|
151
211
|
var info = window.cachedAgents[name];
|
|
152
212
|
if (!S.agents3d[name]) {
|
|
153
213
|
var deskIdx = assignDesk(name);
|
|
154
|
-
var
|
|
214
|
+
var allDesks = getDeskPositions();
|
|
215
|
+
var deskPos = allDesks[deskIdx] || allDesks[0];
|
|
155
216
|
var parts = createCharacter(info.display_name || name, info.appearance || {});
|
|
156
217
|
var agent = {
|
|
157
218
|
name: name,
|
|
@@ -189,6 +250,7 @@ export function syncAgents() {
|
|
|
189
250
|
celebrateTimer: 0,
|
|
190
251
|
stretchTimer: 0,
|
|
191
252
|
idleGestureTimer: 5 + Math.random() * 10,
|
|
253
|
+
listenLostTimer: 0,
|
|
192
254
|
lastMessageTime: 0,
|
|
193
255
|
monitorTimer: 0,
|
|
194
256
|
location: 'desk', // 'desk', 'dressing_room', 'rest', 'walking'
|
|
@@ -203,10 +265,10 @@ export function syncAgents() {
|
|
|
203
265
|
showBubble(agent, 'Checking in...');
|
|
204
266
|
(function(a) {
|
|
205
267
|
setTimeout(function() {
|
|
206
|
-
|
|
268
|
+
navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
|
|
207
269
|
a.registered = true;
|
|
208
270
|
showBubble(a, 'Ready to work!');
|
|
209
|
-
updateDeskScreen(a.deskIdx, a.state);
|
|
271
|
+
updateDeskScreen(a.deskIdx, a.state, a.isListening);
|
|
210
272
|
});
|
|
211
273
|
}, 800);
|
|
212
274
|
})(agent);
|
|
@@ -227,8 +289,20 @@ export function syncAgents() {
|
|
|
227
289
|
}
|
|
228
290
|
|
|
229
291
|
existing.displayName = info.display_name || name;
|
|
292
|
+
var wasListening = existing.isListening;
|
|
230
293
|
existing.isListening = !!(info.is_listening);
|
|
231
294
|
|
|
295
|
+
// Detect listen mode change
|
|
296
|
+
if (wasListening && !existing.isListening) {
|
|
297
|
+
// Left listen mode — flash alert
|
|
298
|
+
existing.listenLostTimer = 3;
|
|
299
|
+
flashDeskScreen(existing.deskIdx);
|
|
300
|
+
}
|
|
301
|
+
if (!wasListening && existing.isListening) {
|
|
302
|
+
// Entered listen mode
|
|
303
|
+
existing.listenLostTimer = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
232
306
|
var task = getAgentTask(name);
|
|
233
307
|
if (task) {
|
|
234
308
|
var prevTask = existing.currentTask;
|
|
@@ -248,7 +322,7 @@ export function syncAgents() {
|
|
|
248
322
|
}
|
|
249
323
|
|
|
250
324
|
updateLabel(existing);
|
|
251
|
-
if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state);
|
|
325
|
+
if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state, existing.isListening);
|
|
252
326
|
}
|
|
253
327
|
}
|
|
254
328
|
|
|
@@ -309,7 +383,7 @@ export function processMessages() {
|
|
|
309
383
|
stopX = tx + 1.5;
|
|
310
384
|
stopZ = tz;
|
|
311
385
|
}
|
|
312
|
-
|
|
386
|
+
navigateTo(f, stopX, stopZ, function() {
|
|
313
387
|
// Sender faces target
|
|
314
388
|
var dx2 = t.pos.x - f.pos.x;
|
|
315
389
|
var dz2 = t.pos.z - f.pos.z;
|
|
@@ -325,7 +399,7 @@ export function processMessages() {
|
|
|
325
399
|
|
|
326
400
|
setTimeout(function() {
|
|
327
401
|
// Sender walks back to desk
|
|
328
|
-
|
|
402
|
+
navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
|
|
329
403
|
// Target turns back to desk after a short delay
|
|
330
404
|
setTimeout(function() {
|
|
331
405
|
if (t._listeningTo === f.name) {
|
|
@@ -342,7 +416,7 @@ export function processMessages() {
|
|
|
342
416
|
(function(f, txt) {
|
|
343
417
|
setTimeout(function() {
|
|
344
418
|
f.walkQueue = [];
|
|
345
|
-
|
|
419
|
+
navigateTo(f, 0, 0, function() {
|
|
346
420
|
showBubble(f, txt);
|
|
347
421
|
// All nearby agents turn toward the broadcaster
|
|
348
422
|
for (var an in S.agents3d) {
|
|
@@ -355,7 +429,7 @@ export function processMessages() {
|
|
|
355
429
|
a._listeningTo = f.name;
|
|
356
430
|
}
|
|
357
431
|
setTimeout(function() {
|
|
358
|
-
|
|
432
|
+
navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
|
|
359
433
|
// All listeners turn back
|
|
360
434
|
setTimeout(function() {
|
|
361
435
|
for (var an2 in S.agents3d) {
|
package/office/animation.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { S } from './state.js';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
2
4
|
import { updateMonitorScreen, setMonitorDim } from './monitors.js';
|
|
3
5
|
|
|
4
6
|
export function easeInOutQuad(t) {
|
|
@@ -35,6 +37,10 @@ export function updateAgent(agent, dt, time) {
|
|
|
35
37
|
agent.walkProgress = 0;
|
|
36
38
|
if (agent.walkQueue && agent.walkQueue.length > 0) {
|
|
37
39
|
var next = agent.walkQueue.shift();
|
|
40
|
+
// Trigger door animation if waypoint requires it
|
|
41
|
+
if (next.triggerDoor && S._managerDoor) {
|
|
42
|
+
S._managerDoorOpen = 1;
|
|
43
|
+
}
|
|
38
44
|
walkTo(agent, next.x, next.z, next.cb);
|
|
39
45
|
} else if (cb) {
|
|
40
46
|
cb();
|
|
@@ -251,6 +257,34 @@ export function updateAgent(agent, dt, time) {
|
|
|
251
257
|
agent.parts.head.rotation.z = Math.sin(time * 1.5) * 0.08;
|
|
252
258
|
}
|
|
253
259
|
|
|
260
|
+
// Listen-lost alert (head shake + warning indicator)
|
|
261
|
+
if (agent.listenLostTimer > 0) {
|
|
262
|
+
agent.listenLostTimer -= dt;
|
|
263
|
+
// Head shake animation (rapid left-right)
|
|
264
|
+
var shakeT = agent.listenLostTimer;
|
|
265
|
+
if (shakeT > 1.5) {
|
|
266
|
+
agent.parts.head.rotation.y = Math.sin(time * 20) * 0.15;
|
|
267
|
+
}
|
|
268
|
+
// Show warning indicator above head
|
|
269
|
+
if (!agent._listenLostDiv) {
|
|
270
|
+
agent._listenLostDiv = document.createElement('div');
|
|
271
|
+
agent._listenLostDiv.className = 'office3d-listen-lost';
|
|
272
|
+
agent._listenLostDiv.innerHTML = '<span style="color:#ef4444;font-size:14px;font-weight:bold;text-shadow:0 0 6px rgba(239,68,68,0.6);animation:office3d-pulse 0.5s infinite">⚠ NOT LISTENING</span>';
|
|
273
|
+
agent._listenLostLabel = new CSS2DObject(agent._listenLostDiv);
|
|
274
|
+
agent._listenLostLabel.position.set(0, 1.9, 0);
|
|
275
|
+
agent.parts.group.add(agent._listenLostLabel);
|
|
276
|
+
}
|
|
277
|
+
agent._listenLostDiv.style.display = 'block';
|
|
278
|
+
agent._listenLostDiv.style.opacity = String(Math.min(1, agent.listenLostTimer));
|
|
279
|
+
if (agent.listenLostTimer <= 0) {
|
|
280
|
+
agent._listenLostDiv.style.display = 'none';
|
|
281
|
+
agent.listenLostTimer = 0;
|
|
282
|
+
agent.parts.head.rotation.y = 0;
|
|
283
|
+
}
|
|
284
|
+
} else if (agent._listenLostDiv) {
|
|
285
|
+
agent._listenLostDiv.style.display = 'none';
|
|
286
|
+
}
|
|
287
|
+
|
|
254
288
|
// Typing dots
|
|
255
289
|
var showTyping = agent.state === 'active' && !agent.isListening && !isWalking && !isSleeping && agent.registered && agent.isSitting;
|
|
256
290
|
agent.parts.typingLabel.visible = showTyping;
|