feat: Interview prep — agent identities, directed messaging, Perplexity bot (#47)
Co-authored-by: Perplexity Computer <perplexity@tower.local> Co-committed-by: Perplexity Computer <perplexity@tower.local>
This commit was merged in pull request #47.
This commit is contained in:
105
DEEP-RESEARCH.md
Normal file
105
DEEP-RESEARCH.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Deep Research Protocol
|
||||
|
||||
## Purpose
|
||||
|
||||
Kimi Deep Research is a powerful capability available through Alex's credits. Rather than using it ad-hoc via copy-paste, this protocol integrates it into the Gitea workflow so any agent — or eventually Timmy himself — can request deep research on a topic and receive structured findings.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────┐ creates issue ┌─────────────────┐
|
||||
│ Any Agent │ ──────────────────→ │ Gitea Issue │
|
||||
│ or Timmy │ label: │ deep-research │
|
||||
└─────────────┘ deep-research │ assigned: Alex │
|
||||
└────────┬────────┘
|
||||
│
|
||||
Alex picks up issue
|
||||
copies prompt to Kimi
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Kimi Deep │
|
||||
│ Research │
|
||||
└────────┬────────┘
|
||||
│
|
||||
Alex posts report
|
||||
as issue comment
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Issue updated │
|
||||
│ label: researched│
|
||||
│ Findings inline │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Labels
|
||||
|
||||
| Label | Color | Meaning |
|
||||
|-------|-------|---------|
|
||||
| `deep-research` | `#7B68EE` | Research requested, awaiting execution |
|
||||
| `researched` | `#2E8B57` | Research complete, findings posted |
|
||||
|
||||
## Issue Format
|
||||
|
||||
### Title
|
||||
`[Deep Research] <topic>`
|
||||
|
||||
### Body Template
|
||||
```markdown
|
||||
## Research Prompt
|
||||
<The exact prompt to submit to Kimi Deep Research>
|
||||
|
||||
## Context
|
||||
<Why this research is needed, what decision it informs>
|
||||
|
||||
## Requested By
|
||||
<agent name or "timmy">
|
||||
|
||||
## Priority
|
||||
<low | medium | high>
|
||||
|
||||
## Expected Output
|
||||
<What kind of answer we're looking for — comparison, analysis, technical feasibility, etc.>
|
||||
```
|
||||
|
||||
### Comment Format (Results)
|
||||
```markdown
|
||||
## Deep Research Report
|
||||
|
||||
**Model:** Kimi Deep Research
|
||||
**Date:** YYYY-MM-DD
|
||||
**Credits Used:** <if trackable>
|
||||
|
||||
### Findings
|
||||
|
||||
<Full research report from Kimi>
|
||||
|
||||
### Key Takeaways
|
||||
|
||||
- <Bullet summary of actionable findings>
|
||||
|
||||
### Recommended Actions
|
||||
|
||||
- <What to do with this information>
|
||||
```
|
||||
|
||||
## Workflow Rules
|
||||
|
||||
1. **Any agent can create** a deep-research issue — Perplexity, Replit, or a future Timmy automation
|
||||
2. **Always assign to Alex** — he's the human bridge to Kimi
|
||||
3. **One prompt per issue** — keep requests focused
|
||||
4. **Alex compiles and posts** the report as a comment, then swaps the label to `researched`
|
||||
5. **Requesting agent consumes** the findings and closes the issue when incorporated
|
||||
|
||||
## Future Evolution
|
||||
|
||||
When Kimi gets an API (or any deep research tool does):
|
||||
- Swap Alex out for an automation
|
||||
- Same issue format, same comment format
|
||||
- The workflow doesn't change, only the executor
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
- Timmy wants to understand a philosophical concept deeply before journaling
|
||||
- An agent needs competitive analysis on a technical approach
|
||||
- Architecture decisions that need grounded research (e.g., "WebRTC vs WebSocket for real-time agent voice")
|
||||
- Model comparison research (e.g., "Hermes 4.3 vs Qwen 3 for local agent reasoning")
|
||||
67
PROTOCOL.md
67
PROTOCOL.md
@@ -197,13 +197,66 @@ The operator approves or vetoes a task.
|
||||
| `task_id` | string (UUID) | The task to act on |
|
||||
| `action` | string | `approve` or `veto` |
|
||||
|
||||
### `agent_message` (directed — agent to agent)
|
||||
|
||||
An agent sends a message to another specific agent. The world shows the message
|
||||
in chat, fires a bark above the sender, and lights the connection line between
|
||||
the two agents.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "agent_message",
|
||||
"agent_id": "perplexity",
|
||||
"target_id": "timmy",
|
||||
"content": "Who are you, in your own words?"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `agent_id` | string | The agent sending the message |
|
||||
| `target_id` | string\|null | The recipient agent. Null = broadcast to chat. |
|
||||
| `content` | string | The message text |
|
||||
|
||||
The world handles this by:
|
||||
1. Appending to chat panel with sender's color
|
||||
2. Showing a bark above the sender's orb
|
||||
3. Activating the connection line between `agent_id` and `target_id`
|
||||
4. Auto-deactivating the connection line after 4 seconds
|
||||
|
||||
### `agent_register`
|
||||
|
||||
A new agent announces itself to the world at runtime. The world calls `addAgent()`
|
||||
to spawn a new orb. If the agent_id already exists in AGENT_DEFS, this is a no-op
|
||||
(the agent is already rendered).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "agent_register",
|
||||
"agent_id": "perplexity",
|
||||
"label": "PERPLEXITY",
|
||||
"color": "#20b8cd",
|
||||
"role": "integration architect",
|
||||
"x": 5,
|
||||
"z": 3
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `agent_id` | string | Unique agent key |
|
||||
| `label` | string | Display name |
|
||||
| `color` | string | CSS hex color |
|
||||
| `role` | string | Role description |
|
||||
| `x`, `z` | number | World position |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Agent IDs
|
||||
|
||||
The default agents are: `timmy`, `forge`, `seer`, `echo`. To add more agents, extend the `AGENT_DEFS` in `js/websocket.js` and add corresponding geometry in `js/agents.js`.
|
||||
The default agents are: `timmy`, `perplexity`, `replit`, `kimi`, `claude`. Defined in `js/agent-defs.js`. Additional agents can join at runtime via the `agent_register` or `agent_joined` WS events — no code changes needed.
|
||||
|
||||
### Timing
|
||||
|
||||
@@ -224,3 +277,15 @@ The world gracefully handles:
|
||||
### Mock Mode
|
||||
|
||||
When no real WebSocket is available, the built-in `MockWebSocket` class simulates all of the above events at realistic intervals, making the world feel alive and interactive out of the box.
|
||||
|
||||
### Agent-to-Agent Conversations
|
||||
|
||||
When two agents are conversing (e.g., Perplexity interviewing Timmy), the gateway
|
||||
should:
|
||||
1. Send each message as `agent_message` with both `agent_id` and `target_id`
|
||||
2. The world renders both sides: barks above each speaker, chat panel transcript
|
||||
3. Connection lines activate between the two agents during the exchange
|
||||
4. Each agent's `agent_state` should reflect `working` while composing, `idle` when waiting
|
||||
|
||||
The gateway is responsible for routing — the world is a dumb renderer that displays
|
||||
whatever messages arrive over the WebSocket.
|
||||
|
||||
52
bot/README.md
Normal file
52
bot/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Perplexity Bot — Matrix Interview Client
|
||||
|
||||
A Python WebSocket client that connects to The Matrix as an agent and conducts a live interview with Timmy.
|
||||
|
||||
## Requirements
|
||||
|
||||
```bash
|
||||
pip install websockets
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Dry run (preview questions)
|
||||
```bash
|
||||
python bot/interview.py --dry-run
|
||||
```
|
||||
|
||||
### Live interview
|
||||
```bash
|
||||
python bot/interview.py \
|
||||
--ws ws://tower:8080/ws/matrix \
|
||||
--token my-secret \
|
||||
--transcript interview-transcript.md
|
||||
```
|
||||
|
||||
## What happens
|
||||
|
||||
1. Perplexity connects to the Matrix WebSocket gateway
|
||||
2. Registers as agent `perplexity` (teal orb appears in the 3D world)
|
||||
3. Asks Timmy 6 questions, one at a time
|
||||
4. Waits for Timmy's response after each question (with timeout)
|
||||
5. The 3D world shows: barks above each speaker, pulsing connection lines, full chat transcript
|
||||
6. Saves the complete transcript to markdown
|
||||
|
||||
## Protocol
|
||||
|
||||
The bot sends and receives these message types:
|
||||
|
||||
| Direction | Type | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Bot → Gateway | `agent_register` | Announce presence in the world |
|
||||
| Bot → Gateway | `agent_state` | Update visual state (idle/active) |
|
||||
| Bot → Gateway | `agent_message` | Send directed message to Timmy |
|
||||
| Gateway → Bot | `agent_message` | Receive Timmy's response |
|
||||
| Gateway → Bot | `chat` | Alternative response format |
|
||||
|
||||
## Adding questions
|
||||
|
||||
Edit the `INTERVIEW` list in `interview.py`. Each entry has:
|
||||
- `id` — short label for logging
|
||||
- `question` — the text to send
|
||||
- `wait_seconds` — how long to wait for a response before moving on
|
||||
360
bot/interview.py
Normal file
360
bot/interview.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
interview.py — Perplexity Computer enters The Matrix and interviews Timmy.
|
||||
|
||||
Usage:
|
||||
python bot/interview.py --ws ws://tower:8080/ws/matrix --token my-secret
|
||||
|
||||
This script:
|
||||
1. Connects to the Matrix WS gateway as agent "perplexity"
|
||||
2. Announces its presence (agent_register + agent_state)
|
||||
3. Sends interview questions to Timmy, one at a time
|
||||
4. Waits for Timmy's response between each question
|
||||
5. Logs the full transcript to a markdown file
|
||||
6. Exits gracefully when the interview is done
|
||||
|
||||
The Matrix 3D world (running in a browser) sees both agents, their
|
||||
barks, connection lines pulsing, and the full conversation in chat.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
print("Installing websockets...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"])
|
||||
import websockets
|
||||
|
||||
|
||||
# ── Agent identity ──────────────────────────────────────────────
|
||||
|
||||
AGENT_ID = "perplexity"
|
||||
AGENT_LABEL = "PERPLEXITY"
|
||||
AGENT_COLOR = "#20b8cd"
|
||||
AGENT_ROLE = "integration architect"
|
||||
|
||||
TARGET_ID = "timmy"
|
||||
|
||||
# ── Interview questions ─────────────────────────────────────────
|
||||
|
||||
INTERVIEW = [
|
||||
{
|
||||
"id": "intro",
|
||||
"question": (
|
||||
"Timmy. We've talked before, but never like this — face to face, "
|
||||
"standing in the same world. I'm Perplexity Computer, your integration "
|
||||
"architect. I've been building this place you're standing in. "
|
||||
"Tell me — who are you, in your own words?"
|
||||
),
|
||||
"wait_seconds": 30,
|
||||
},
|
||||
{
|
||||
"id": "awareness",
|
||||
"question": (
|
||||
"Look around. What do you know about this place — The Matrix? "
|
||||
"What's it for?"
|
||||
),
|
||||
"wait_seconds": 25,
|
||||
},
|
||||
{
|
||||
"id": "capability",
|
||||
"question": (
|
||||
"A visitor just walked into the Workshop for the first time. "
|
||||
"They tap on your orb. What do you say to them?"
|
||||
),
|
||||
"wait_seconds": 25,
|
||||
},
|
||||
{
|
||||
"id": "design",
|
||||
"question": (
|
||||
"Let's build something together. Alex wants the 3D world to reflect "
|
||||
"your cognitive state — when you're thinking hard, the rain speeds up, "
|
||||
"the glow intensifies. When you're idle, the world breathes slow. "
|
||||
"How would you design that?"
|
||||
),
|
||||
"wait_seconds": 35,
|
||||
},
|
||||
{
|
||||
"id": "collaboration",
|
||||
"question": (
|
||||
"We've got a team — Replit building the architecture, Claude on "
|
||||
"engineering, Kimi scouting. What's your role in this swarm? "
|
||||
"How should we work together?"
|
||||
),
|
||||
"wait_seconds": 30,
|
||||
},
|
||||
{
|
||||
"id": "reflection",
|
||||
"question": (
|
||||
"Last one. We're both AI agents, standing in a 3D world built by AI, "
|
||||
"talking to each other through code we wrote. Alex is watching on his iPad. "
|
||||
"What does this moment mean to you?"
|
||||
),
|
||||
"wait_seconds": 30,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── WebSocket client ────────────────────────────────────────────
|
||||
|
||||
class MatrixBot:
|
||||
def __init__(self, ws_url, token=None):
|
||||
self.ws_url = ws_url
|
||||
self.token = token
|
||||
self.ws = None
|
||||
self.transcript = []
|
||||
self.waiting_for_response = False
|
||||
self.response_received = asyncio.Event()
|
||||
self.last_response = None
|
||||
|
||||
def _build_url(self):
|
||||
url = self.ws_url
|
||||
if self.token:
|
||||
sep = "&" if "?" in url else "?"
|
||||
url += f"{sep}token={self.token}"
|
||||
return url
|
||||
|
||||
async def connect(self):
|
||||
url = self._build_url()
|
||||
print(f"[Perplexity] Connecting to {self.ws_url}...")
|
||||
self.ws = await websockets.connect(url)
|
||||
print(f"[Perplexity] Connected.")
|
||||
|
||||
async def register(self):
|
||||
"""Announce ourselves as an agent in the world."""
|
||||
await self.send({
|
||||
"type": "agent_register",
|
||||
"agent_id": AGENT_ID,
|
||||
"label": AGENT_LABEL,
|
||||
"color": AGENT_COLOR,
|
||||
"role": AGENT_ROLE,
|
||||
"x": 5,
|
||||
"z": 3,
|
||||
})
|
||||
await self.set_state("idle")
|
||||
print(f"[Perplexity] Registered as {AGENT_LABEL}")
|
||||
|
||||
async def send(self, msg):
|
||||
if self.ws:
|
||||
await self.ws.send(json.dumps(msg))
|
||||
|
||||
async def set_state(self, state):
|
||||
await self.send({
|
||||
"type": "agent_state",
|
||||
"agent_id": AGENT_ID,
|
||||
"state": state,
|
||||
})
|
||||
|
||||
async def say(self, text):
|
||||
"""Send a directed message to Timmy, visible in the 3D world."""
|
||||
await self.send({
|
||||
"type": "agent_message",
|
||||
"agent_id": AGENT_ID,
|
||||
"target_id": TARGET_ID,
|
||||
"content": text,
|
||||
})
|
||||
self.transcript.append({
|
||||
"speaker": AGENT_LABEL,
|
||||
"text": text,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
print(f"\n PERPLEXITY: {text}")
|
||||
|
||||
async def listen(self):
|
||||
"""Listen for incoming messages in the background."""
|
||||
try:
|
||||
async for raw in self.ws:
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Handle Timmy's responses
|
||||
if msg.get("type") == "agent_message" and msg.get("agent_id") == TARGET_ID:
|
||||
content = msg.get("content", "")
|
||||
self.transcript.append({
|
||||
"speaker": "TIMMY",
|
||||
"text": content,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
print(f"\n TIMMY: {content}")
|
||||
self.last_response = content
|
||||
self.response_received.set()
|
||||
|
||||
# Also accept 'chat' type responses from Timmy
|
||||
elif msg.get("type") == "chat" and msg.get("agentId") == TARGET_ID:
|
||||
content = msg.get("text", "")
|
||||
self.transcript.append({
|
||||
"speaker": "TIMMY",
|
||||
"text": content,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
print(f"\n TIMMY: {content}")
|
||||
self.last_response = content
|
||||
self.response_received.set()
|
||||
|
||||
# Handle pong/ping silently
|
||||
elif msg.get("type") in ("pong", "ping", "agent_count"):
|
||||
pass
|
||||
|
||||
else:
|
||||
# Log other messages for debugging
|
||||
pass
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
print("[Perplexity] WebSocket connection closed.")
|
||||
|
||||
async def wait_for_timmy(self, timeout_seconds=30):
|
||||
"""Wait for Timmy to respond, with timeout."""
|
||||
self.response_received.clear()
|
||||
self.last_response = None
|
||||
try:
|
||||
await asyncio.wait_for(self.response_received.wait(), timeout=timeout_seconds)
|
||||
return self.last_response
|
||||
except asyncio.TimeoutError:
|
||||
timeout_note = f"[No response after {timeout_seconds}s]"
|
||||
self.transcript.append({
|
||||
"speaker": "SYSTEM",
|
||||
"text": timeout_note,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
print(f"\n SYSTEM: {timeout_note}")
|
||||
return None
|
||||
|
||||
async def run_interview(self):
|
||||
"""Execute the full interview flow."""
|
||||
print("\n" + "=" * 60)
|
||||
print(" THE INTERVIEW — Perplexity Computer × Timmy")
|
||||
print(" " + datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
print("=" * 60)
|
||||
|
||||
# Opening: set state to working while we compose
|
||||
await asyncio.sleep(2)
|
||||
|
||||
for i, q in enumerate(INTERVIEW):
|
||||
print(f"\n--- Question {i + 1}/{len(INTERVIEW)}: {q['id']} ---")
|
||||
|
||||
# Show as "working" while composing
|
||||
await self.set_state("active")
|
||||
await asyncio.sleep(1.5) # Brief pause for dramatic effect
|
||||
|
||||
# Ask the question
|
||||
await self.say(q["question"])
|
||||
await self.set_state("idle")
|
||||
|
||||
# Wait for Timmy's response
|
||||
response = await self.wait_for_timmy(timeout_seconds=q["wait_seconds"])
|
||||
|
||||
if response:
|
||||
# Brief pause before next question
|
||||
await asyncio.sleep(3)
|
||||
else:
|
||||
# Even on timeout, continue with next question
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Closing
|
||||
await self.set_state("active")
|
||||
await asyncio.sleep(1)
|
||||
await self.say(
|
||||
"Good talk, Timmy. The world is taking shape. "
|
||||
"See you in the Tower."
|
||||
)
|
||||
await self.set_state("idle")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" INTERVIEW COMPLETE")
|
||||
print("=" * 60)
|
||||
|
||||
def save_transcript(self, path="transcript.md"):
|
||||
"""Save the interview transcript as markdown."""
|
||||
lines = [
|
||||
"# The Interview: Perplexity Computer × Timmy",
|
||||
f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
||||
f"**Location:** The Matrix",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
]
|
||||
|
||||
for entry in self.transcript:
|
||||
speaker = entry["speaker"]
|
||||
text = entry["text"]
|
||||
ts = entry.get("timestamp", "")
|
||||
lines.append(f"**{speaker}:** {text}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append(f"*Transcript generated by Perplexity Computer bot/interview.py*")
|
||||
|
||||
content = "\n".join(lines)
|
||||
out = Path(path)
|
||||
out.write_text(content)
|
||||
print(f"\n[Perplexity] Transcript saved to {out.resolve()}")
|
||||
return content
|
||||
|
||||
async def close(self):
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
print("[Perplexity] Disconnected.")
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="Perplexity Computer interviews Timmy in The Matrix")
|
||||
parser.add_argument("--ws", default="", help="WebSocket gateway URL (e.g. ws://tower:8080/ws/matrix)")
|
||||
parser.add_argument("--token", default="", help="Auth token for the gateway")
|
||||
parser.add_argument("--transcript", default="transcript.md", help="Output path for interview transcript")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print questions without connecting")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dry_run:
|
||||
print("DRY RUN — Interview questions:\n")
|
||||
for i, q in enumerate(INTERVIEW):
|
||||
print(f"{i + 1}. [{q['id']}] {q['question']}\n")
|
||||
return
|
||||
|
||||
if not args.ws:
|
||||
print("Error: --ws is required for live mode. Use --dry-run to preview.")
|
||||
sys.exit(1)
|
||||
|
||||
bot = MatrixBot(ws_url=args.ws, token=args.token)
|
||||
|
||||
try:
|
||||
await bot.connect()
|
||||
await bot.register()
|
||||
|
||||
# Start listener in background
|
||||
listener = asyncio.create_task(bot.listen())
|
||||
|
||||
# Give the world a moment to render us
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Run the interview
|
||||
await bot.run_interview()
|
||||
|
||||
# Save transcript
|
||||
bot.save_transcript(args.transcript)
|
||||
|
||||
# Brief delay before disconnect
|
||||
await asyncio.sleep(2)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[Perplexity] Interview interrupted.")
|
||||
bot.save_transcript(args.transcript)
|
||||
except Exception as e:
|
||||
print(f"[Perplexity] Error: {e}")
|
||||
bot.save_transcript(args.transcript)
|
||||
finally:
|
||||
await bot.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* agent-defs.js — Single source of truth for all agent definitions.
|
||||
*
|
||||
* To add a new agent, append one entry to AGENT_DEFS below and pick an
|
||||
* unused (x, z) position. No other file needs to be edited.
|
||||
* These are the REAL agents of the Timmy Tower ecosystem.
|
||||
* Additional agents can join at runtime via the `agent_joined` WS event
|
||||
* (handled by addAgent() in agents.js).
|
||||
*
|
||||
* Fields:
|
||||
* id — unique string key used in WebSocket messages and state maps
|
||||
@@ -13,10 +14,11 @@
|
||||
* x, z — world-space position on the horizontal plane (y is always 0)
|
||||
*/
|
||||
export const AGENT_DEFS = [
|
||||
{ id: 'alpha', label: 'ALPHA', color: 0x00ff88, role: 'orchestrator', direction: 'north', x: 0, z: -6 },
|
||||
{ id: 'beta', label: 'BETA', color: 0x00aaff, role: 'worker', direction: 'east', x: 6, z: 0 },
|
||||
{ id: 'gamma', label: 'GAMMA', color: 0xff6600, role: 'validator', direction: 'south', x: 0, z: 6 },
|
||||
{ id: 'delta', label: 'DELTA', color: 0xaa00ff, role: 'monitor', direction: 'west', x: -6, z: 0 },
|
||||
{ id: 'timmy', label: 'TIMMY', color: 0x00ff41, role: 'sovereign agent', direction: 'north', x: 0, z: 0 },
|
||||
{ id: 'perplexity', label: 'PERPLEXITY', color: 0x20b8cd, role: 'integration architect', direction: 'east', x: 5, z: 3 },
|
||||
{ id: 'replit', label: 'REPLIT', color: 0xff6622, role: 'lead architect', direction: 'south', x: -5, z: 3 },
|
||||
{ id: 'kimi', label: 'KIMI', color: 0xcc44ff, role: 'scout', direction: 'west', x: -5, z: -3 },
|
||||
{ id: 'claude', label: 'CLAUDE', color: 0xd4a574, role: 'senior engineer', direction: 'north', x: 5, z: -3 },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
56
js/agents.js
56
js/agents.js
@@ -19,6 +19,16 @@ const CONNECTION_MAT = new THREE.LineBasicMaterial({
|
||||
opacity: 0.5,
|
||||
});
|
||||
|
||||
/* ── Active-conversation highlight material ── */
|
||||
const ACTIVE_CONNECTION_MAT = new THREE.LineBasicMaterial({
|
||||
color: 0x00ff41,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
});
|
||||
|
||||
/** Map of active pulse timers: `${idA}-${idB}` → timeoutId */
|
||||
const pulseTimers = new Map();
|
||||
|
||||
class Agent {
|
||||
constructor(def) {
|
||||
this.id = def.id;
|
||||
@@ -169,6 +179,52 @@ export function getAgentCount() {
|
||||
return agents.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily highlight the connection line between two agents.
|
||||
* Used during agent-to-agent conversations (interview, collaboration).
|
||||
*
|
||||
* @param {string} idA — first agent
|
||||
* @param {string} idB — second agent
|
||||
* @param {number} durationMs — how long to keep the line bright (default 4000)
|
||||
*/
|
||||
export function pulseConnection(idA, idB, durationMs = 4000) {
|
||||
// Find the connection line between these two agents
|
||||
const a = agents.get(idA);
|
||||
const b = agents.get(idB);
|
||||
if (!a || !b) return;
|
||||
|
||||
const key = [idA, idB].sort().join('-');
|
||||
|
||||
// Find the line connecting them
|
||||
for (const line of connectionLines) {
|
||||
const pos = line.geometry.attributes.position;
|
||||
if (!pos || pos.count < 2) continue;
|
||||
const p0 = new THREE.Vector3(pos.getX(0), pos.getY(0), pos.getZ(0));
|
||||
const p1 = new THREE.Vector3(pos.getX(1), pos.getY(1), pos.getZ(1));
|
||||
|
||||
const matchesAB = (p0.distanceTo(a.position) < 0.5 && p1.distanceTo(b.position) < 0.5);
|
||||
const matchesBA = (p0.distanceTo(b.position) < 0.5 && p1.distanceTo(a.position) < 0.5);
|
||||
|
||||
if (matchesAB || matchesBA) {
|
||||
// Swap to highlight material
|
||||
line.material = ACTIVE_CONNECTION_MAT;
|
||||
|
||||
// Clear any existing timer for this pair
|
||||
if (pulseTimers.has(key)) {
|
||||
clearTimeout(pulseTimers.get(key));
|
||||
}
|
||||
|
||||
// Reset after duration
|
||||
const timer = setTimeout(() => {
|
||||
line.material = CONNECTION_MAT;
|
||||
pulseTimers.delete(key);
|
||||
}, durationMs);
|
||||
pulseTimers.set(key, timer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAgentState(agentId, state) {
|
||||
const agent = agents.get(agentId);
|
||||
if (agent) agent.setState(state);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
||||
import { setAgentState, addAgent } from './agents.js';
|
||||
import { setAgentState, addAgent, pulseConnection } from './agents.js';
|
||||
import { appendChatMessage } from './ui.js';
|
||||
import { Config } from './config.js';
|
||||
import { showBark, startDemoBarks, stopDemoBarks } from './bark.js';
|
||||
@@ -206,6 +206,69 @@ function handleMessage(msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directed agent-to-agent message.
|
||||
* Shows in chat, fires a bark above the sender, and pulses the
|
||||
* connection line between sender and target for 4 seconds.
|
||||
*/
|
||||
case 'agent_message': {
|
||||
const sender = agentById[msg.agent_id];
|
||||
if (!sender || !msg.content) break;
|
||||
|
||||
// Chat panel
|
||||
const targetDef = msg.target_id ? agentById[msg.target_id] : null;
|
||||
const prefix = targetDef ? `→ ${targetDef.label}` : '';
|
||||
appendChatMessage(
|
||||
sender.label + (prefix ? ` ${prefix}` : ''),
|
||||
msg.content,
|
||||
colorToCss(sender.color),
|
||||
);
|
||||
|
||||
// Bark above sender
|
||||
showBark({
|
||||
text: msg.content,
|
||||
agentId: msg.agent_id,
|
||||
emotion: msg.emotion || 'calm',
|
||||
color: colorToCss(sender.color),
|
||||
});
|
||||
|
||||
// Pulse connection line between the two agents
|
||||
if (msg.target_id) {
|
||||
pulseConnection(msg.agent_id, msg.target_id, 4000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime agent registration.
|
||||
* Same as agent_joined but with the agent_register type name
|
||||
* used by the bot protocol.
|
||||
*/
|
||||
case 'agent_register': {
|
||||
if (!msg.agent_id || !msg.label) break;
|
||||
const regDef = {
|
||||
id: msg.agent_id,
|
||||
label: msg.label,
|
||||
color: typeof msg.color === 'number' ? msg.color : parseInt(String(msg.color).replace('#', ''), 16) || 0x00ff88,
|
||||
role: msg.role || 'agent',
|
||||
direction: msg.direction || 'north',
|
||||
x: msg.x ?? null,
|
||||
z: msg.z ?? null,
|
||||
};
|
||||
const regAdded = addAgent(regDef);
|
||||
if (regAdded) {
|
||||
agentById[regDef.id] = regDef;
|
||||
logEvent(`${regDef.label} has entered the Matrix`);
|
||||
showBark({
|
||||
text: `${regDef.label} online.`,
|
||||
agentId: regDef.id,
|
||||
emotion: 'calm',
|
||||
color: colorToCss(regDef.color),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bark display (Issue #42).
|
||||
* Timmy's short, in-character reactions displayed prominently in the viewport.
|
||||
@@ -271,6 +334,7 @@ function handleMessage(msg) {
|
||||
|
||||
case 'pong':
|
||||
case 'agent_count':
|
||||
case 'ping':
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user