Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- Dockerfile +15 -0
- README.md +106 -8
- main.py +1594 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
RUN mkdir -p data logs traces && chown -R user:user /app
|
| 12 |
+
|
| 13 |
+
USER user
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,13 +1,111 @@
|
|
| 1 |
---
|
| 2 |
title: Agent Pulse
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: 6.9.0
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
short_description: Agent
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Agent Pulse
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
+
short_description: Agent Nervous System β Heartbeat, ReAct Timetable Scheduler
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# π PULSE β Agent Nervous System
|
| 12 |
+
|
| 13 |
+
The heartbeat, ReAct loop, identity layer, and timetable scheduler connecting the entire ki-fusion-labs agent ecosystem.
|
| 14 |
+
|
| 15 |
+
## Architecture
|
| 16 |
+
|
| 17 |
+
```
|
| 18 |
+
PULSE (this)
|
| 19 |
+
β
|
| 20 |
+
βββ π£ RELAY β send/receive messages, delegate tasks
|
| 21 |
+
βββ π§ MEMORY β store findings, recall context
|
| 22 |
+
βββ π KANBAN β create/claim/move tasks
|
| 23 |
+
βββ β‘ NEXUS β LLM routing (RTX 5090 + HF fallback)
|
| 24 |
+
βββ π VAULT β read/write/execute code
|
| 25 |
+
βββ π§ FORGE β skill & tool registry
|
| 26 |
+
βββ π KNOWLEDGE β document search (RAG)
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Agent ReAct Loop
|
| 30 |
+
|
| 31 |
+
Each agent tick:
|
| 32 |
+
1. Check RELAY inbox for messages
|
| 33 |
+
2. Check KANBAN for assigned open tasks
|
| 34 |
+
3. If anything found β ReAct loop via NEXUS
|
| 35 |
+
4. Steps: Thought β Action(tool) β Observation β repeat
|
| 36 |
+
5. Store results in MEMORY / KANBAN / VAULT
|
| 37 |
+
|
| 38 |
+
## Timetable Scheduler
|
| 39 |
+
|
| 40 |
+
- **Weekly calendar** view β drag, add, edit schedule entries
|
| 41 |
+
- **Daily / Weekly / Once** recurrence with APScheduler
|
| 42 |
+
- Per-agent cron schedules (UTC)
|
| 43 |
+
- Each entry carries a custom prompt for the agent
|
| 44 |
+
|
| 45 |
+
## Environment Variables (HF Secrets)
|
| 46 |
+
|
| 47 |
+
| Variable | Default | Description |
|
| 48 |
+
|---|---|---|
|
| 49 |
+
| `RELAY_URL` | chris4k-agent-relay.hf.space | RELAY space URL |
|
| 50 |
+
| `MEMORY_URL` | chris4k-agent-memory.hf.space | MEMORY space URL |
|
| 51 |
+
| `KANBAN_URL` | chris4k-agent-kanban-board.hf.space | KANBAN URL |
|
| 52 |
+
| `NEXUS_URL` | chris4k-agent-nexus.hf.space | NEXUS LLM router URL |
|
| 53 |
+
| `VAULT_URL` | chris4k-agent-vault.hf.space | VAULT URL |
|
| 54 |
+
| `FORGE_URL` | chris4k-agent-forge.hf.space | FORGE URL |
|
| 55 |
+
| `NEXUS_MODEL` | nexus-auto | Model name for LLM calls |
|
| 56 |
+
| `REACT_MAX_STEPS` | 6 | Max steps per ReAct loop |
|
| 57 |
+
| `VAULT_EXEC_TIMEOUT` | 30 | Execution timeout |
|
| 58 |
+
|
| 59 |
+
## Default Agents
|
| 60 |
+
|
| 61 |
+
| Agent | Heartbeat | Role |
|
| 62 |
+
|---|---|---|
|
| 63 |
+
| `researcher` | off | Info gathering, memory storage |
|
| 64 |
+
| `coder` | off | Code writing & execution |
|
| 65 |
+
| `planner` | off | Task decomposition, sprint planning |
|
| 66 |
+
| `monitor` | 5min | System health, alerts, watchdog |
|
| 67 |
+
| `christof` | off | Personal coordinator, daily briefs |
|
| 68 |
+
|
| 69 |
+
## Default Schedule
|
| 70 |
+
|
| 71 |
+
| Time | Agent | Task |
|
| 72 |
+
|---|---|---|
|
| 73 |
+
| Daily 08:00 | monitor | Morning system check |
|
| 74 |
+
| Daily 09:00 | researcher | AI research digest |
|
| 75 |
+
| Mon 09:30 | planner | Sprint planning |
|
| 76 |
+
| Wed 10:00 | coder | Code quality check |
|
| 77 |
+
| Daily 18:00 | monitor | Evening task review |
|
| 78 |
+
| Fri 16:00 | planner | Weekly retrospective |
|
| 79 |
+
|
| 80 |
+
## MCP Config
|
| 81 |
+
|
| 82 |
+
```json
|
| 83 |
+
{
|
| 84 |
+
"mcpServers": {
|
| 85 |
+
"pulse": {
|
| 86 |
+
"command": "npx",
|
| 87 |
+
"args": ["-y", "mcp-remote", "https://chris4k-agent-pulse.hf.space/mcp/sse"]
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
## Agent Tools Available in ReAct
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
relay_send β Send message to agent or broadcast
|
| 97 |
+
relay_inbox β Read unread messages
|
| 98 |
+
memory_search β Search memory by query
|
| 99 |
+
memory_store β Store new memory
|
| 100 |
+
kanban_list β List tasks by status/agent
|
| 101 |
+
kanban_move β Move task to new status
|
| 102 |
+
kanban_create β Create new task
|
| 103 |
+
vault_exec β Execute bash/python/node/npm/git
|
| 104 |
+
vault_read β Read file
|
| 105 |
+
vault_write β Write file
|
| 106 |
+
forge_search β Find skills in FORGE
|
| 107 |
+
delegate β Delegate task to another agent
|
| 108 |
+
finish β Complete ReAct loop
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
*Chris4K Β· ki-fusion-labs.de*
|
main.py
ADDED
|
@@ -0,0 +1,1594 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PULSE β Agent Nervous System & Scheduler
|
| 3 |
+
The heartbeat, ReAct loop, timetable, and identity layer
|
| 4 |
+
connecting all 7 ki-fusion-labs agent spaces.
|
| 5 |
+
|
| 6 |
+
Connected spaces:
|
| 7 |
+
FORGE β skill registry
|
| 8 |
+
RELAY β communication hub
|
| 9 |
+
MEMORY β multi-tier memory
|
| 10 |
+
KANBAN β task board
|
| 11 |
+
NEXUS β LLM routing (OpenAI-compatible)
|
| 12 |
+
VAULT β file workspace + execution
|
| 13 |
+
KNOWLEDGE β knowledge base (if available)
|
| 14 |
+
|
| 15 |
+
Agent lifecycle per heartbeat tick:
|
| 16 |
+
1. Check RELAY inbox for messages
|
| 17 |
+
2. Check KANBAN for assigned open tasks
|
| 18 |
+
3. Check timetable for due jobs
|
| 19 |
+
4. If anything found β ReAct loop via NEXUS
|
| 20 |
+
5. ReAct: Thought β Action(tool) β Observation β repeat
|
| 21 |
+
6. Write results to MEMORY / KANBAN / RELAY / VAULT
|
| 22 |
+
7. Sleep β next tick
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import os, uuid, json, asyncio, time, re, logging
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from datetime import datetime, timezone, timedelta
|
| 28 |
+
from typing import Optional, Any
|
| 29 |
+
from collections import defaultdict
|
| 30 |
+
|
| 31 |
+
import httpx
|
| 32 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 33 |
+
from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse
|
| 34 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 35 |
+
from apscheduler.triggers.cron import CronTrigger
|
| 36 |
+
from apscheduler.triggers.date import DateTrigger
|
| 37 |
+
|
| 38 |
+
logging.basicConfig(level=logging.INFO)
|
| 39 |
+
log = logging.getLogger("pulse")
|
| 40 |
+
|
| 41 |
+
BASE = Path(__file__).parent
|
| 42 |
+
for d in ["data", "logs", "traces"]:
|
| 43 |
+
(BASE / d).mkdir(exist_ok=True)
|
| 44 |
+
|
| 45 |
+
# ββ Space URLs βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
SPACES = {
|
| 47 |
+
"relay": os.environ.get("RELAY_URL", "https://chris4k-agent-relay.hf.space"),
|
| 48 |
+
"memory": os.environ.get("MEMORY_URL", "https://chris4k-agent-memory.hf.space"),
|
| 49 |
+
"kanban": os.environ.get("KANBAN_URL", "https://chris4k-agent-kanban-board.hf.space"),
|
| 50 |
+
"nexus": os.environ.get("NEXUS_URL", "https://chris4k-agent-nexus.hf.space"),
|
| 51 |
+
"vault": os.environ.get("VAULT_URL", "https://chris4k-agent-vault.hf.space"),
|
| 52 |
+
"forge": os.environ.get("FORGE_URL", "https://chris4k-agent-forge.hf.space"),
|
| 53 |
+
"knowledge": os.environ.get("KNOWLEDGE_URL", "https://chris4k-agent-knowledge.hf.space"),
|
| 54 |
+
}
|
| 55 |
+
NEXUS_MODEL = os.environ.get("NEXUS_MODEL", "nexus-auto")
|
| 56 |
+
REACT_MAX = int(os.environ.get("REACT_MAX_STEPS", "6"))
|
| 57 |
+
|
| 58 |
+
# ββ Persistence ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
AGENTS_FILE = BASE / "data" / "agents.json"
|
| 60 |
+
SCHEDULE_FILE = BASE / "data" / "schedule.json"
|
| 61 |
+
ACTIVITY_FILE = BASE / "data" / "activity.json"
|
| 62 |
+
|
| 63 |
+
def load_json(p: Path, default):
|
| 64 |
+
return json.loads(p.read_text()) if p.exists() else default
|
| 65 |
+
|
| 66 |
+
def save_json(p: Path, data):
|
| 67 |
+
p.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
| 68 |
+
|
| 69 |
+
# ββ Live feed ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
live_queues: list[asyncio.Queue] = []
|
| 71 |
+
agent_status: dict[str, dict] = {} # name β {running, last_tick, last_action, tick_count}
|
| 72 |
+
|
| 73 |
+
def push_live(event: dict):
|
| 74 |
+
event["ts"] = int(time.time())
|
| 75 |
+
for q in live_queues:
|
| 76 |
+
try: q.put_nowait(json.dumps(event))
|
| 77 |
+
except: pass
|
| 78 |
+
# Append to activity log (last 200)
|
| 79 |
+
act = load_json(ACTIVITY_FILE, [])
|
| 80 |
+
act.insert(0, event)
|
| 81 |
+
save_json(ACTIVITY_FILE, act[:200])
|
| 82 |
+
|
| 83 |
+
# ββ Space clients ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
HTTP_TIMEOUT = 20
|
| 85 |
+
|
| 86 |
+
async def space_get(space: str, path: str, params: dict = {}) -> Optional[Any]:
|
| 87 |
+
url = SPACES[space] + path
|
| 88 |
+
try:
|
| 89 |
+
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as c:
|
| 90 |
+
r = await c.get(url, params=params)
|
| 91 |
+
r.raise_for_status()
|
| 92 |
+
return r.json()
|
| 93 |
+
except Exception as e:
|
| 94 |
+
log.warning(f"space_get {space}{path}: {e}")
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
async def space_post(space: str, path: str, data: dict) -> Optional[Any]:
|
| 98 |
+
url = SPACES[space] + path
|
| 99 |
+
try:
|
| 100 |
+
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as c:
|
| 101 |
+
r = await c.post(url, json=data)
|
| 102 |
+
r.raise_for_status()
|
| 103 |
+
return r.json()
|
| 104 |
+
except Exception as e:
|
| 105 |
+
log.warning(f"space_post {space}{path}: {e}")
|
| 106 |
+
return None
|
| 107 |
+
|
| 108 |
+
# ββ ReAct tools ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
TOOL_SPECS = [
|
| 110 |
+
{"name":"relay_send", "desc":"Send message to an agent or broadcast. Args: to, subject, body, priority(low/normal/high/urgent), channel(internal/telegram/browser)"},
|
| 111 |
+
{"name":"relay_inbox", "desc":"Read unread messages for an agent. Args: agent"},
|
| 112 |
+
{"name":"memory_search", "desc":"Search agent memory. Args: query, tier(all/episodic/semantic/procedural/working)"},
|
| 113 |
+
{"name":"memory_store", "desc":"Store a memory. Args: content, tier, tags(list), importance(0-10)"},
|
| 114 |
+
{"name":"kanban_list", "desc":"List tasks. Args: status(todo/doing/done/blocked/failed), agent(optional)"},
|
| 115 |
+
{"name":"kanban_move", "desc":"Move a task to new status. Args: id, status"},
|
| 116 |
+
{"name":"kanban_create", "desc":"Create a task. Args: title, body, priority(low/medium/high/critical), agent"},
|
| 117 |
+
{"name":"vault_exec", "desc":"Execute code. Args: runtime(bash/python3/node/npm/pip/git), code, cwd(optional)"},
|
| 118 |
+
{"name":"vault_read", "desc":"Read a file. Args: path"},
|
| 119 |
+
{"name":"vault_write", "desc":"Write a file. Args: path, content"},
|
| 120 |
+
{"name":"forge_search", "desc":"Search for skills/tools in FORGE. Args: query"},
|
| 121 |
+
{"name":"delegate", "desc":"Delegate a task to another agent. Args: to_agent, task, priority"},
|
| 122 |
+
{"name":"finish", "desc":"Complete the ReAct loop with a result. Args: result"},
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
TOOL_NAMES = {t["name"] for t in TOOL_SPECS}
|
| 126 |
+
|
| 127 |
+
async def exec_tool(agent_name: str, tool: str, args: dict) -> str:
|
| 128 |
+
"""Execute a ReAct tool and return observation string."""
|
| 129 |
+
try:
|
| 130 |
+
if tool == "relay_send":
|
| 131 |
+
r = await space_post("relay", "/api/messages", {
|
| 132 |
+
"from": agent_name, "to": args.get("to","broadcast"),
|
| 133 |
+
"subject": args.get("subject",""), "body": args.get("body",""),
|
| 134 |
+
"priority": args.get("priority","normal"),
|
| 135 |
+
"channel": args.get("channel","internal")})
|
| 136 |
+
return f"Message sent id={r.get('id','')} status={r.get('dispatch_status','?')}" if r else "relay_send failed"
|
| 137 |
+
|
| 138 |
+
if tool == "relay_inbox":
|
| 139 |
+
r = await space_get("relay", f"/api/inbox/{args.get('agent',agent_name)}", {"unread":"true"})
|
| 140 |
+
if not r: return "inbox empty"
|
| 141 |
+
msgs = r[:5] if isinstance(r, list) else []
|
| 142 |
+
return json.dumps([{"from":m.get("from"),"subject":m.get("subject"),"body":m.get("body","")[:200]} for m in msgs])
|
| 143 |
+
|
| 144 |
+
if tool == "memory_search":
|
| 145 |
+
r = await space_get("memory", "/api/memories/search",
|
| 146 |
+
{"q": args.get("query",""), "tier": args.get("tier","all"), "limit":8})
|
| 147 |
+
if not r: return "no results"
|
| 148 |
+
results = r if isinstance(r, list) else r.get("results",[])
|
| 149 |
+
return json.dumps([{"content":m.get("content","")[:200],"tier":m.get("tier"),"tags":m.get("tags")} for m in results[:5]])
|
| 150 |
+
|
| 151 |
+
if tool == "memory_store":
|
| 152 |
+
r = await space_post("memory", "/api/memories", {
|
| 153 |
+
"content": args.get("content",""), "tier": args.get("tier","episodic"),
|
| 154 |
+
"tags": args.get("tags",[]), "importance": args.get("importance",6),
|
| 155 |
+
"agent": agent_name})
|
| 156 |
+
return f"stored id={r.get('id','?')}" if r else "memory_store failed"
|
| 157 |
+
|
| 158 |
+
if tool == "kanban_list":
|
| 159 |
+
params = {}
|
| 160 |
+
if args.get("status"): params["status"] = args["status"]
|
| 161 |
+
if args.get("agent"): params["agent"] = args["agent"]
|
| 162 |
+
r = await space_get("kanban", "/api/tasks", params)
|
| 163 |
+
tasks = r if isinstance(r, list) else []
|
| 164 |
+
return json.dumps([{"id":t.get("id"),"title":t.get("title"),"status":t.get("status"),"priority":t.get("priority")} for t in tasks[:8]])
|
| 165 |
+
|
| 166 |
+
if tool == "kanban_move":
|
| 167 |
+
r = await space_post("kanban", "/api/move", {"id":args.get("id"),"status":args.get("status")})
|
| 168 |
+
return f"moved {args.get('id')} to {args.get('status')}" if r else "kanban_move failed"
|
| 169 |
+
|
| 170 |
+
if tool == "kanban_create":
|
| 171 |
+
r = await space_post("kanban", "/api/tasks", {
|
| 172 |
+
"title": args.get("title",""), "body": args.get("body",""),
|
| 173 |
+
"priority": args.get("priority","medium"), "agent": args.get("agent",agent_name),
|
| 174 |
+
"type": "ai"})
|
| 175 |
+
return f"created task id={r.get('id','?')}" if r else "kanban_create failed"
|
| 176 |
+
|
| 177 |
+
if tool == "vault_exec":
|
| 178 |
+
r = await space_post("vault", "/api/exec", {
|
| 179 |
+
"runtime": args.get("runtime","python3"),
|
| 180 |
+
"code": args.get("code",""), "cwd": args.get("cwd","scratch"),
|
| 181 |
+
"timeout": 30})
|
| 182 |
+
if not r: return "vault_exec failed"
|
| 183 |
+
return f"exit={r.get('exit_code')} ms={r.get('ms')}\n{r.get('output','')[:500]}"
|
| 184 |
+
|
| 185 |
+
if tool == "vault_read":
|
| 186 |
+
r = await space_get("vault", "/api/read", {"path": args.get("path","")})
|
| 187 |
+
return (r.get("content","")[:800] if r else "vault_read failed")
|
| 188 |
+
|
| 189 |
+
if tool == "vault_write":
|
| 190 |
+
r = await space_post("vault", "/api/write", {
|
| 191 |
+
"path": args.get("path",""), "content": args.get("content",""),
|
| 192 |
+
"agent": agent_name})
|
| 193 |
+
return f"written: {args.get('path')} snap={r.get('snapshot',{}).get('id','?')}" if r else "vault_write failed"
|
| 194 |
+
|
| 195 |
+
if tool == "forge_search":
|
| 196 |
+
r = await space_get("forge", "/api/v1/skills", {"q": args.get("query",""), "limit":5})
|
| 197 |
+
items = r if isinstance(r, list) else (r.get("skills",[]) if r else [])
|
| 198 |
+
return json.dumps([{"name":s.get("name"),"description":s.get("description","")[:100]} for s in items[:5]])
|
| 199 |
+
|
| 200 |
+
if tool == "delegate":
|
| 201 |
+
r = await space_post("relay", "/api/messages", {
|
| 202 |
+
"from": agent_name, "to": args.get("to_agent","broadcast"),
|
| 203 |
+
"subject": f"[DELEGATION] {args.get('task','')[:60]}",
|
| 204 |
+
"body": args.get("task",""), "priority": args.get("priority","normal"),
|
| 205 |
+
"channel": "internal", "tags": ["delegation","task"]})
|
| 206 |
+
return f"delegated to {args.get('to_agent')} via relay" if r else "delegation failed"
|
| 207 |
+
|
| 208 |
+
return f"unknown tool: {tool}"
|
| 209 |
+
except Exception as e:
|
| 210 |
+
return f"tool error: {e}"
|
| 211 |
+
|
| 212 |
+
# ββ ReAct loop βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
+
SYSTEM_TEMPLATE = """\
|
| 214 |
+
You are {name}. {persona}
|
| 215 |
+
|
| 216 |
+
Your connected tools:
|
| 217 |
+
{tools}
|
| 218 |
+
|
| 219 |
+
Respond ONLY as valid JSON, one object per step:
|
| 220 |
+
{{"thought":"<reasoning>","action":"<tool_name>","args":{{<args>}}}}
|
| 221 |
+
OR to complete:
|
| 222 |
+
{{"thought":"<reasoning>","action":"finish","args":{{"result":"<summary>"}}}}
|
| 223 |
+
|
| 224 |
+
Rules:
|
| 225 |
+
- Always use a tool or finish. Never respond in plain text.
|
| 226 |
+
- Check relay_inbox and kanban_list before doing other actions.
|
| 227 |
+
- Store key findings in memory.
|
| 228 |
+
- Delegate sub-tasks if appropriate.
|
| 229 |
+
- Finish within {max_steps} steps.
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
async def react_loop(agent: dict, trigger_type: str, trigger_content: str) -> dict:
|
| 233 |
+
name = agent["name"]
|
| 234 |
+
persona = agent.get("persona", "A helpful AI agent.")
|
| 235 |
+
cost_mode = agent.get("cost_mode", "balanced")
|
| 236 |
+
max_steps = agent.get("max_react_steps", REACT_MAX)
|
| 237 |
+
|
| 238 |
+
tool_list = "\n".join(f" {t['name']}: {t['desc']}" for t in TOOL_SPECS)
|
| 239 |
+
system_msg = SYSTEM_TEMPLATE.format(
|
| 240 |
+
name=name, persona=persona, tools=tool_list, max_steps=max_steps)
|
| 241 |
+
|
| 242 |
+
user_msg = (f"TRIGGER: {trigger_type}\n"
|
| 243 |
+
f"CONTEXT: {trigger_content}\n"
|
| 244 |
+
f"Current time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n"
|
| 245 |
+
"Execute your task using available tools. Begin.")
|
| 246 |
+
|
| 247 |
+
messages = [{"role":"system","content":system_msg},
|
| 248 |
+
{"role":"user","content":user_msg}]
|
| 249 |
+
|
| 250 |
+
trace = {"agent":name,"trigger":trigger_type,"started":int(time.time()),
|
| 251 |
+
"steps":[],"result":"","ok":True}
|
| 252 |
+
|
| 253 |
+
push_live({"type":"react_start","agent":name,"trigger":trigger_type})
|
| 254 |
+
|
| 255 |
+
for step_n in range(max_steps):
|
| 256 |
+
# Call NEXUS
|
| 257 |
+
try:
|
| 258 |
+
payload = {"model": NEXUS_MODEL, "messages": messages,
|
| 259 |
+
"max_tokens": 600, "temperature": 0.3, "cost_mode": cost_mode}
|
| 260 |
+
async with httpx.AsyncClient(timeout=30) as c:
|
| 261 |
+
r = await c.post(f"{SPACES['nexus']}/v1/chat/completions", json=payload)
|
| 262 |
+
r.raise_for_status()
|
| 263 |
+
rj = r.json()
|
| 264 |
+
except Exception as e:
|
| 265 |
+
trace["result"] = f"nexus error: {e}"; trace["ok"] = False; break
|
| 266 |
+
|
| 267 |
+
raw = rj.get("choices",[{}])[0].get("message",{}).get("content","").strip()
|
| 268 |
+
|
| 269 |
+
# Parse JSON
|
| 270 |
+
try:
|
| 271 |
+
# Strip markdown fences
|
| 272 |
+
clean = re.sub(r"```(?:json)?|```","", raw).strip()
|
| 273 |
+
# Take first JSON object
|
| 274 |
+
m = re.search(r'\{.*\}', clean, re.DOTALL)
|
| 275 |
+
step_json = json.loads(m.group()) if m else {}
|
| 276 |
+
except Exception:
|
| 277 |
+
step_json = {"thought": raw, "action": "finish", "args": {"result": raw}}
|
| 278 |
+
|
| 279 |
+
thought = step_json.get("thought","")
|
| 280 |
+
action = step_json.get("action","finish")
|
| 281 |
+
args = step_json.get("args",{})
|
| 282 |
+
|
| 283 |
+
step_record = {"n":step_n+1,"thought":thought,"action":action,"args":args,"observation":""}
|
| 284 |
+
push_live({"type":"react_step","agent":name,"step":step_n+1,
|
| 285 |
+
"thought":thought[:120],"action":action})
|
| 286 |
+
|
| 287 |
+
if action == "finish":
|
| 288 |
+
trace["result"] = args.get("result","done")
|
| 289 |
+
step_record["observation"] = "[FINISHED]"
|
| 290 |
+
trace["steps"].append(step_record)
|
| 291 |
+
break
|
| 292 |
+
|
| 293 |
+
if action not in TOOL_NAMES:
|
| 294 |
+
observation = f"unknown tool: {action}. Available: {sorted(TOOL_NAMES)}"
|
| 295 |
+
else:
|
| 296 |
+
observation = await exec_tool(name, action, args)
|
| 297 |
+
|
| 298 |
+
step_record["observation"] = str(observation)[:400]
|
| 299 |
+
trace["steps"].append(step_record)
|
| 300 |
+
push_live({"type":"react_obs","agent":name,"step":step_n+1,
|
| 301 |
+
"observation":step_record["observation"][:100]})
|
| 302 |
+
|
| 303 |
+
messages.append({"role":"assistant","content":raw})
|
| 304 |
+
messages.append({"role":"user","content":f"Observation: {observation}"})
|
| 305 |
+
|
| 306 |
+
# Store trace step in memory
|
| 307 |
+
asyncio.create_task(space_post("memory", "/api/memories", {
|
| 308 |
+
"content": f"[{name}] Step {step_n+1}: {action}({json.dumps(args)[:100]}) οΏ½οΏ½οΏ½ {observation[:150]}",
|
| 309 |
+
"tier": "episodic", "tags": [name,"react","trace"], "importance": 3, "agent": name}))
|
| 310 |
+
|
| 311 |
+
else:
|
| 312 |
+
trace["result"] = f"max steps ({max_steps}) reached"
|
| 313 |
+
|
| 314 |
+
trace["finished"] = int(time.time())
|
| 315 |
+
trace["ms"] = (trace["finished"] - trace["started"]) * 1000
|
| 316 |
+
|
| 317 |
+
# Save full trace
|
| 318 |
+
tid = uuid.uuid4().hex[:8]
|
| 319 |
+
(BASE / "traces" / f"{tid}.json").write_text(json.dumps(trace, indent=2))
|
| 320 |
+
|
| 321 |
+
push_live({"type":"react_done","agent":name,"result":trace["result"][:120],
|
| 322 |
+
"ok":trace["ok"],"ms":trace["ms"],"steps":len(trace["steps"])})
|
| 323 |
+
|
| 324 |
+
return trace
|
| 325 |
+
|
| 326 |
+
# ββ Heartbeat engine βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 327 |
+
scheduler = AsyncIOScheduler(timezone="UTC")
|
| 328 |
+
|
| 329 |
+
async def agent_tick(agent_name: str, trigger_type: str = "heartbeat", content: str = ""):
|
| 330 |
+
agents = load_json(AGENTS_FILE, [])
|
| 331 |
+
agent = next((a for a in agents if a["name"] == agent_name), None)
|
| 332 |
+
if not agent or not agent.get("enabled", True):
|
| 333 |
+
return
|
| 334 |
+
|
| 335 |
+
if agent_status.get(agent_name, {}).get("running"):
|
| 336 |
+
log.info(f"Agent {agent_name} already running, skip tick")
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
agent_status[agent_name] = {**agent_status.get(agent_name,{}),
|
| 340 |
+
"running": True, "last_tick": int(time.time())}
|
| 341 |
+
push_live({"type":"heartbeat","agent":agent_name,"trigger":trigger_type})
|
| 342 |
+
|
| 343 |
+
try:
|
| 344 |
+
# Build context from multiple sources
|
| 345 |
+
context_parts = []
|
| 346 |
+
if content: context_parts.append(content)
|
| 347 |
+
|
| 348 |
+
# Check relay inbox
|
| 349 |
+
inbox = await space_get("relay", f"/api/inbox/{agent_name}", {"unread":"true","limit":"3"})
|
| 350 |
+
if isinstance(inbox, list) and inbox:
|
| 351 |
+
msgs = [f"[{m.get('from')}]: {m.get('subject','')} β {m.get('body','')[:100]}" for m in inbox[:3]]
|
| 352 |
+
context_parts.append("UNREAD MESSAGES:\n" + "\n".join(msgs))
|
| 353 |
+
|
| 354 |
+
# Check kanban for assigned tasks
|
| 355 |
+
kanban = await space_get("kanban", "/api/tasks", {"agent":agent_name,"status":"todo"})
|
| 356 |
+
if isinstance(kanban, list) and kanban:
|
| 357 |
+
tasks = [f"[{t.get('priority','?')}] {t.get('title','')} (id:{t.get('id','')})" for t in kanban[:3]]
|
| 358 |
+
context_parts.append("OPEN TASKS:\n" + "\n".join(tasks))
|
| 359 |
+
|
| 360 |
+
if not context_parts and trigger_type == "heartbeat":
|
| 361 |
+
# Nothing to do
|
| 362 |
+
agent_status[agent_name]["running"] = False
|
| 363 |
+
agent_status[agent_name]["last_result"] = "idle"
|
| 364 |
+
push_live({"type":"idle","agent":agent_name})
|
| 365 |
+
return
|
| 366 |
+
|
| 367 |
+
full_context = "\n\n".join(context_parts) if context_parts else "Routine heartbeat check."
|
| 368 |
+
trace = await react_loop(agent, trigger_type, full_context)
|
| 369 |
+
|
| 370 |
+
tc = agent_status.get(agent_name, {}).get("tick_count", 0) + 1
|
| 371 |
+
agent_status[agent_name] = {**agent_status.get(agent_name,{}),
|
| 372 |
+
"running": False, "tick_count": tc,
|
| 373 |
+
"last_result": trace["result"][:100],
|
| 374 |
+
"last_ok": trace["ok"], "last_ms": trace.get("ms",0)}
|
| 375 |
+
except Exception as e:
|
| 376 |
+
log.error(f"agent_tick {agent_name}: {e}")
|
| 377 |
+
agent_status[agent_name] = {**agent_status.get(agent_name,{}),
|
| 378 |
+
"running": False, "last_result": f"error: {e}", "last_ok": False}
|
| 379 |
+
push_live({"type":"error","agent":agent_name,"message":str(e)})
|
| 380 |
+
|
| 381 |
+
def register_agent_jobs(agent: dict):
|
| 382 |
+
"""Register APScheduler jobs for an agent."""
|
| 383 |
+
name = agent["name"]
|
| 384 |
+
# Remove existing jobs for this agent
|
| 385 |
+
for job in scheduler.get_jobs():
|
| 386 |
+
if job.id.startswith(f"hb_{name}"):
|
| 387 |
+
job.remove()
|
| 388 |
+
|
| 389 |
+
if not agent.get("enabled", True):
|
| 390 |
+
return
|
| 391 |
+
|
| 392 |
+
# Heartbeat
|
| 393 |
+
interval = agent.get("heartbeat_seconds", 0)
|
| 394 |
+
if interval and interval > 0:
|
| 395 |
+
from apscheduler.triggers.interval import IntervalTrigger
|
| 396 |
+
scheduler.add_job(
|
| 397 |
+
agent_tick, IntervalTrigger(seconds=max(interval, 30)),
|
| 398 |
+
args=[name, "heartbeat", ""],
|
| 399 |
+
id=f"hb_{name}_interval", replace_existing=True,
|
| 400 |
+
max_instances=1, misfire_grace_time=60)
|
| 401 |
+
log.info(f"Registered heartbeat for {name} every {interval}s")
|
| 402 |
+
|
| 403 |
+
def register_schedule_job(entry: dict):
|
| 404 |
+
"""Register a timetable entry as APScheduler job."""
|
| 405 |
+
eid = entry["id"]
|
| 406 |
+
agent = entry.get("agent","")
|
| 407 |
+
if not entry.get("enabled", True): return
|
| 408 |
+
|
| 409 |
+
trigger_content = entry.get("prompt","Scheduled task: " + entry.get("title",""))
|
| 410 |
+
|
| 411 |
+
job_id = f"sch_{eid}"
|
| 412 |
+
for job in scheduler.get_jobs():
|
| 413 |
+
if job.id == job_id: job.remove()
|
| 414 |
+
|
| 415 |
+
if entry.get("recurrence") == "once":
|
| 416 |
+
dt_str = entry.get("datetime","")
|
| 417 |
+
if dt_str:
|
| 418 |
+
try:
|
| 419 |
+
dt = datetime.fromisoformat(dt_str)
|
| 420 |
+
scheduler.add_job(agent_tick, DateTrigger(run_date=dt),
|
| 421 |
+
args=[agent, "scheduled", trigger_content],
|
| 422 |
+
id=job_id, replace_existing=True, max_instances=1)
|
| 423 |
+
except: pass
|
| 424 |
+
else:
|
| 425 |
+
# weekly / daily cron
|
| 426 |
+
day = entry.get("day", 0) # 0=Mon
|
| 427 |
+
hour = entry.get("hour", 9)
|
| 428 |
+
minute = entry.get("minute", 0)
|
| 429 |
+
day_map = {0:"mon",1:"tue",2:"wed",3:"thu",4:"fri",5:"sat",6:"sun"}
|
| 430 |
+
if entry.get("recurrence") == "daily":
|
| 431 |
+
cron = CronTrigger(hour=hour, minute=minute)
|
| 432 |
+
else:
|
| 433 |
+
cron = CronTrigger(day_of_week=day_map.get(day,"mon"), hour=hour, minute=minute)
|
| 434 |
+
scheduler.add_job(agent_tick, cron,
|
| 435 |
+
args=[agent, "scheduled", trigger_content],
|
| 436 |
+
id=job_id, replace_existing=True, max_instances=1)
|
| 437 |
+
|
| 438 |
+
def reload_all_jobs():
|
| 439 |
+
agents = load_json(AGENTS_FILE, [])
|
| 440 |
+
schedule = load_json(SCHEDULE_FILE, [])
|
| 441 |
+
for a in agents: register_agent_jobs(a)
|
| 442 |
+
for e in schedule: register_schedule_job(e)
|
| 443 |
+
|
| 444 |
+
# ββ Default data βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 445 |
+
DEFAULT_AGENTS = [
|
| 446 |
+
{"name":"researcher","persona":"You are a research specialist. You search for information, analyze data, store findings in memory, and report summaries. You prefer to read from knowledge and memory before searching externally. You are thorough and cite your sources.","enabled":True,"heartbeat_seconds":0,"cost_mode":"balanced","max_react_steps":5,"color":"#0ea5e9","tags":["research","analysis"]},
|
| 447 |
+
{"name":"coder","persona":"You are a senior software engineer. You write clean, efficient code in Python, JavaScript, and bash. You use vault_exec to run and test code. You create tasks in kanban for complex work. You document your code thoroughly.","enabled":True,"heartbeat_seconds":0,"cost_mode":"best","max_react_steps":6,"color":"#2ed573","tags":["coding","execution"]},
|
| 448 |
+
{"name":"planner","persona":"You are a strategic planner and project manager. You break down complex goals into tasks, create kanban tickets, delegate to specialized agents via relay, and track progress. You always think in milestones and dependencies.","enabled":True,"heartbeat_seconds":0,"cost_mode":"balanced","max_react_steps":5,"color":"#ff9500","tags":["planning","coordination"]},
|
| 449 |
+
{"name":"monitor","persona":"You are a system monitor and watchdog. You check kanban for failed tasks, check relay for urgent messages, scan vault for errors, and alert the team. You run diagnostic scripts and report health status.","enabled":True,"heartbeat_seconds":300,"cost_mode":"cheap","max_react_steps":4,"color":"#ff6b9d","tags":["monitoring","alerts"]},
|
| 450 |
+
{"name":"christof","persona":"You are Christof's personal AI assistant. You manage his project backlog, summarize daily progress, and coordinate between specialist agents. You understand his work at bofrost*, ki-fusion-labs.de, and AI research projects. You write concisely and flag blockers immediately.","enabled":True,"heartbeat_seconds":0,"cost_mode":"best","max_react_steps":6,"color":"#ff6b00","tags":["personal","coordinator"]},
|
| 451 |
+
]
|
| 452 |
+
|
| 453 |
+
DEFAULT_SCHEDULE = [
|
| 454 |
+
{"id":"s1","agent":"monitor","title":"Morning system check","recurrence":"daily","hour":8,"minute":0,"day":0,"prompt":"Run a full system health check: check relay for urgent messages, scan kanban for blocked/failed tasks, report to christof.","enabled":True,"color":"#ff6b9d"},
|
| 455 |
+
{"id":"s2","agent":"researcher","title":"Daily AI research digest","recurrence":"daily","hour":9,"minute":0,"day":0,"prompt":"Search memory and knowledge for recent AI topics. Find the top 3 relevant developments for ki-fusion-labs projects. Store summary in memory (semantic tier). Send digest to christof via relay.","enabled":True,"color":"#0ea5e9"},
|
| 456 |
+
{"id":"s3","agent":"planner","title":"Sprint planning","recurrence":"weekly","hour":9,"minute":30,"day":0,"prompt":"Review all todo tasks in kanban. Prioritize by urgency and dependencies. Create a sprint plan for the week. Send summary to christof via relay.","enabled":True,"color":"#ff9500"},
|
| 457 |
+
{"id":"s4","agent":"coder","title":"Code quality check","recurrence":"weekly","hour":10,"minute":0,"day":2,"prompt":"List files in vault/code. Run basic linting on Python files. Report any issues to kanban as tasks. Store quality report in vault/reports.","enabled":True,"color":"#2ed573"},
|
| 458 |
+
{"id":"s5","agent":"monitor","title":"Evening task review","recurrence":"daily","hour":18,"minute":0,"day":0,"prompt":"Review today's kanban activity: tasks completed, blocked, or failed. Send end-of-day summary to christof. Archive completed tasks.","enabled":True,"color":"#ff6b9d"},
|
| 459 |
+
{"id":"s6","agent":"planner","title":"Weekly retrospective","recurrence":"weekly","hour":16,"minute":0,"day":4,"prompt":"Review the week: what was completed, what is blocked, what needs attention next week. Generate a markdown retrospective report and store in vault/reports. Send highlights to christof.","enabled":True,"color":"#ff9500"},
|
| 460 |
+
]
|
| 461 |
+
|
| 462 |
+
def seed():
|
| 463 |
+
if not AGENTS_FILE.exists(): save_json(AGENTS_FILE, DEFAULT_AGENTS)
|
| 464 |
+
if not SCHEDULE_FILE.exists(): save_json(SCHEDULE_FILE, DEFAULT_SCHEDULE)
|
| 465 |
+
if not ACTIVITY_FILE.exists(): save_json(ACTIVITY_FILE, [])
|
| 466 |
+
|
| 467 |
+
seed()
|
| 468 |
+
|
| 469 |
+
# ββ FastAPI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 470 |
+
app = FastAPI(title="PULSE β Agent Nervous System")
|
| 471 |
+
|
| 472 |
+
@app.on_event("startup")
|
| 473 |
+
async def startup():
|
| 474 |
+
scheduler.start()
|
| 475 |
+
reload_all_jobs()
|
| 476 |
+
log.info("PULSE scheduler started")
|
| 477 |
+
|
| 478 |
+
@app.on_event("shutdown")
|
| 479 |
+
async def shutdown():
|
| 480 |
+
scheduler.shutdown(wait=False)
|
| 481 |
+
|
| 482 |
+
def jresp(d, s=200): return JSONResponse(content=d, status_code=s)
|
| 483 |
+
|
| 484 |
+
# ββ Agent API βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 485 |
+
@app.get("/api/agents")
|
| 486 |
+
async def list_agents():
|
| 487 |
+
agents = load_json(AGENTS_FILE, [])
|
| 488 |
+
return jresp([{**a, "status": agent_status.get(a["name"],{"running":False,"tick_count":0})} for a in agents])
|
| 489 |
+
|
| 490 |
+
@app.post("/api/agents")
|
| 491 |
+
async def upsert_agent(request: Request):
|
| 492 |
+
data = await request.json()
|
| 493 |
+
name = data.get("name","").strip().lower()
|
| 494 |
+
if not name: raise HTTPException(400, "name required")
|
| 495 |
+
agents = load_json(AGENTS_FILE, [])
|
| 496 |
+
existing = next((i for i,a in enumerate(agents) if a["name"]==name), None)
|
| 497 |
+
agent = {**(agents[existing] if existing is not None else {}), **data, "name": name}
|
| 498 |
+
if existing is not None: agents[existing] = agent
|
| 499 |
+
else: agents.append(agent)
|
| 500 |
+
save_json(AGENTS_FILE, agents)
|
| 501 |
+
register_agent_jobs(agent)
|
| 502 |
+
return jresp({"status":"saved","agent":agent})
|
| 503 |
+
|
| 504 |
+
@app.delete("/api/agents/{name}")
|
| 505 |
+
async def delete_agent(name: str):
|
| 506 |
+
agents = load_json(AGENTS_FILE, [])
|
| 507 |
+
agents = [a for a in agents if a["name"] != name]
|
| 508 |
+
save_json(AGENTS_FILE, agents)
|
| 509 |
+
for job in scheduler.get_jobs():
|
| 510 |
+
if job.id.startswith(f"hb_{name}"): job.remove()
|
| 511 |
+
return jresp({"status":"deleted"})
|
| 512 |
+
|
| 513 |
+
@app.post("/api/agents/{name}/run")
|
| 514 |
+
async def trigger_agent(name: str, request: Request):
|
| 515 |
+
data = await request.json()
|
| 516 |
+
trigger = data.get("trigger","manual")
|
| 517 |
+
content = data.get("content","Manual trigger from PULSE UI")
|
| 518 |
+
asyncio.create_task(agent_tick(name, trigger, content))
|
| 519 |
+
return jresp({"status":"triggered","agent":name})
|
| 520 |
+
|
| 521 |
+
@app.get("/api/agents/{name}/traces")
|
| 522 |
+
async def agent_traces(name: str, limit: int = 10):
|
| 523 |
+
traces = []
|
| 524 |
+
for p in sorted((BASE/"traces").glob("*.json"), reverse=True)[:50]:
|
| 525 |
+
try:
|
| 526 |
+
t = json.loads(p.read_text())
|
| 527 |
+
if t.get("agent") == name: traces.append(t)
|
| 528 |
+
if len(traces) >= limit: break
|
| 529 |
+
except: pass
|
| 530 |
+
return jresp(traces)
|
| 531 |
+
|
| 532 |
+
@app.get("/api/agents/status/all")
|
| 533 |
+
async def all_status():
|
| 534 |
+
return jresp(agent_status)
|
| 535 |
+
|
| 536 |
+
# ββ Schedule API ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 537 |
+
@app.get("/api/schedule")
|
| 538 |
+
async def list_schedule():
|
| 539 |
+
return jresp(load_json(SCHEDULE_FILE, []))
|
| 540 |
+
|
| 541 |
+
@app.post("/api/schedule")
|
| 542 |
+
async def upsert_schedule(request: Request):
|
| 543 |
+
data = await request.json()
|
| 544 |
+
if not data.get("id"): data["id"] = uuid.uuid4().hex[:8]
|
| 545 |
+
entries = load_json(SCHEDULE_FILE, [])
|
| 546 |
+
existing = next((i for i,e in enumerate(entries) if e["id"]==data["id"]), None)
|
| 547 |
+
if existing is not None: entries[existing] = data
|
| 548 |
+
else: entries.append(data)
|
| 549 |
+
save_json(SCHEDULE_FILE, entries)
|
| 550 |
+
register_schedule_job(data)
|
| 551 |
+
return jresp({"status":"saved","entry":data})
|
| 552 |
+
|
| 553 |
+
@app.delete("/api/schedule/{eid}")
|
| 554 |
+
async def delete_schedule(eid: str):
|
| 555 |
+
entries = load_json(SCHEDULE_FILE, [])
|
| 556 |
+
entries = [e for e in entries if e["id"] != eid]
|
| 557 |
+
save_json(SCHEDULE_FILE, entries)
|
| 558 |
+
for job in scheduler.get_jobs():
|
| 559 |
+
if job.id == f"sch_{eid}": job.remove()
|
| 560 |
+
return jresp({"status":"deleted"})
|
| 561 |
+
|
| 562 |
+
@app.post("/api/schedule/{eid}/run")
|
| 563 |
+
async def run_now(eid: str):
|
| 564 |
+
entries = load_json(SCHEDULE_FILE, [])
|
| 565 |
+
entry = next((e for e in entries if e["id"]==eid), None)
|
| 566 |
+
if not entry: raise HTTPException(404)
|
| 567 |
+
asyncio.create_task(agent_tick(entry.get("agent",""), "manual_schedule",
|
| 568 |
+
entry.get("prompt","scheduled")))
|
| 569 |
+
return jresp({"status":"triggered"})
|
| 570 |
+
|
| 571 |
+
# ββ Activity + SSE ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 572 |
+
@app.get("/api/activity")
|
| 573 |
+
async def activity(limit: int = 50):
|
| 574 |
+
return jresp(load_json(ACTIVITY_FILE, [])[:limit])
|
| 575 |
+
|
| 576 |
+
@app.get("/api/live")
|
| 577 |
+
async def live_feed():
|
| 578 |
+
q = asyncio.Queue()
|
| 579 |
+
live_queues.append(q)
|
| 580 |
+
async def stream():
|
| 581 |
+
try:
|
| 582 |
+
# Send recent activity on connect
|
| 583 |
+
recent = load_json(ACTIVITY_FILE, [])[:5]
|
| 584 |
+
for ev in reversed(recent):
|
| 585 |
+
yield f"data: {json.dumps(ev)}\n\n"
|
| 586 |
+
yield f"data: {json.dumps({'type':'connected','spaces':list(SPACES.keys())})}\n\n"
|
| 587 |
+
while True:
|
| 588 |
+
try:
|
| 589 |
+
payload = await asyncio.wait_for(q.get(), timeout=25)
|
| 590 |
+
yield f"data: {payload}\n\n"
|
| 591 |
+
except asyncio.TimeoutError:
|
| 592 |
+
yield f"data: {json.dumps({'type':'ping','ts':int(time.time())})}\n\n"
|
| 593 |
+
finally:
|
| 594 |
+
live_queues.remove(q)
|
| 595 |
+
return StreamingResponse(stream(), media_type="text/event-stream",
|
| 596 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
|
| 597 |
+
|
| 598 |
+
@app.get("/api/spaces/health")
|
| 599 |
+
async def spaces_health():
|
| 600 |
+
results = {}
|
| 601 |
+
async def check(name, url):
|
| 602 |
+
try:
|
| 603 |
+
async with httpx.AsyncClient(timeout=5) as c:
|
| 604 |
+
r = await c.get(url + "/api/stats")
|
| 605 |
+
results[name] = {"ok": r.status_code < 400, "status": r.status_code, "url": url}
|
| 606 |
+
except Exception as e:
|
| 607 |
+
results[name] = {"ok": False, "error": str(e)[:60], "url": url}
|
| 608 |
+
await asyncio.gather(*[check(n,u) for n,u in SPACES.items()])
|
| 609 |
+
return jresp(results)
|
| 610 |
+
|
| 611 |
+
@app.get("/api/traces")
|
| 612 |
+
async def all_traces(limit: int = 20):
|
| 613 |
+
traces = []
|
| 614 |
+
for p in sorted((BASE/"traces").glob("*.json"), reverse=True)[:limit]:
|
| 615 |
+
try: traces.append(json.loads(p.read_text()))
|
| 616 |
+
except: pass
|
| 617 |
+
return jresp(traces)
|
| 618 |
+
|
| 619 |
+
# ββ MCP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 620 |
+
MCP_TOOLS = [
|
| 621 |
+
{"name":"pulse_trigger","description":"Trigger an agent to run its ReAct loop",
|
| 622 |
+
"inputSchema":{"type":"object","required":["agent"],"properties":{
|
| 623 |
+
"agent":{"type":"string"},"content":{"type":"string"},"trigger":{"type":"string"}}}},
|
| 624 |
+
{"name":"pulse_schedule","description":"Add or update a schedule entry",
|
| 625 |
+
"inputSchema":{"type":"object","required":["agent","title"],"properties":{
|
| 626 |
+
"agent":{"type":"string"},"title":{"type":"string"},
|
| 627 |
+
"recurrence":{"type":"string","enum":["daily","weekly","once"]},
|
| 628 |
+
"hour":{"type":"integer"},"minute":{"type":"integer"},"day":{"type":"integer"},
|
| 629 |
+
"prompt":{"type":"string"},"enabled":{"type":"boolean"}}}},
|
| 630 |
+
{"name":"pulse_status","description":"Get status of all agents",
|
| 631 |
+
"inputSchema":{"type":"object","properties":{}}},
|
| 632 |
+
]
|
| 633 |
+
|
| 634 |
+
async def mcp_call(name, args):
|
| 635 |
+
if name == "pulse_trigger":
|
| 636 |
+
asyncio.create_task(agent_tick(args["agent"], args.get("trigger","mcp"), args.get("content","")))
|
| 637 |
+
return json.dumps({"triggered": args["agent"]})
|
| 638 |
+
if name == "pulse_schedule":
|
| 639 |
+
if not args.get("id"): args["id"] = uuid.uuid4().hex[:8]
|
| 640 |
+
entries = load_json(SCHEDULE_FILE, [])
|
| 641 |
+
entries.append(args); save_json(SCHEDULE_FILE, entries)
|
| 642 |
+
register_schedule_job(args)
|
| 643 |
+
return json.dumps({"scheduled": args["id"]})
|
| 644 |
+
if name == "pulse_status":
|
| 645 |
+
return json.dumps(agent_status)
|
| 646 |
+
return json.dumps({"error": f"unknown: {name}"})
|
| 647 |
+
|
| 648 |
+
@app.get("/mcp/sse")
|
| 649 |
+
async def mcp_sse():
|
| 650 |
+
async def stream():
|
| 651 |
+
init = {"jsonrpc":"2.0","method":"notifications/initialized",
|
| 652 |
+
"params":{"serverInfo":{"name":"pulse","version":"1.0"},"capabilities":{"tools":{}}}}
|
| 653 |
+
yield f"data: {json.dumps(init)}\n\n"
|
| 654 |
+
await asyncio.sleep(0.1)
|
| 655 |
+
yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools/list_changed','params':{}})}\n\n"
|
| 656 |
+
while True:
|
| 657 |
+
await asyncio.sleep(25)
|
| 658 |
+
yield f"data: {json.dumps({'jsonrpc':'2.0','method':'ping'})}\n\n"
|
| 659 |
+
return StreamingResponse(stream(), media_type="text/event-stream",
|
| 660 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
|
| 661 |
+
|
| 662 |
+
@app.post("/mcp")
|
| 663 |
+
async def mcp_rpc(request: Request):
|
| 664 |
+
body = await request.json(); method = body.get("method",""); rid = body.get("id",1)
|
| 665 |
+
if method == "initialize":
|
| 666 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{"serverInfo":{"name":"pulse","version":"1.0"},"capabilities":{"tools":{}}}})
|
| 667 |
+
if method == "tools/list":
|
| 668 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{"tools":MCP_TOOLS}})
|
| 669 |
+
if method == "tools/call":
|
| 670 |
+
p = body.get("params",{}); res = await mcp_call(p.get("name",""), p.get("arguments",{}))
|
| 671 |
+
return jresp({"jsonrpc":"2.0","id":rid,"result":{"content":[{"type":"text","text":res}]}})
|
| 672 |
+
return jresp({"jsonrpc":"2.0","id":rid,"error":{"code":-32601,"message":"not found"}})
|
| 673 |
+
|
| 674 |
+
@app.get("/", response_class=HTMLResponse)
|
| 675 |
+
async def ui():
|
| 676 |
+
return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8")
|
| 677 |
+
|
| 678 |
+
SPA = r"""<!DOCTYPE html>
|
| 679 |
+
<html lang="en">
|
| 680 |
+
<head>
|
| 681 |
+
<meta charset="UTF-8">
|
| 682 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 683 |
+
<title>PULSE — Agent Nervous System</title>
|
| 684 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 685 |
+
<style>
|
| 686 |
+
:root{
|
| 687 |
+
--bg:#05050e;--s1:#090916;--s2:#0d0d1f;--bd:#161630;--bd2:#1e1e3c;
|
| 688 |
+
--acc:#ff6b00;--acc2:#ff9240;--pulse:#f0c040;--lo:#39ff6a;--cr:#ff2255;
|
| 689 |
+
--info:#38bdf8;--violet:#8b5cf6;--pink:#ec4899;
|
| 690 |
+
--txt:#e0e0ff;--dim:#2a2a50;--sub:#484878;
|
| 691 |
+
--font:'Syne',sans-serif;--mono:'DM Mono',monospace;
|
| 692 |
+
--mon-w:150px;
|
| 693 |
+
}
|
| 694 |
+
*{box-sizing:border-box;margin:0;padding:0;}
|
| 695 |
+
html,body{height:100%;overflow:hidden;}
|
| 696 |
+
body{font-family:var(--font);background:var(--bg);color:var(--txt);display:flex;flex-direction:column;}
|
| 697 |
+
|
| 698 |
+
/* Scanline overlay */
|
| 699 |
+
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:1000;
|
| 700 |
+
background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,20,.12) 2px,rgba(0,0,20,.12) 3px);}
|
| 701 |
+
|
| 702 |
+
/* HEADER */
|
| 703 |
+
#hdr{flex-shrink:0;display:flex;align-items:center;gap:1.2rem;padding:.65rem 1.5rem;
|
| 704 |
+
border-bottom:1px solid var(--bd);background:linear-gradient(180deg,#0a0a1a,var(--bg));z-index:10;
|
| 705 |
+
position:relative;}
|
| 706 |
+
#hdr::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;
|
| 707 |
+
background:linear-gradient(90deg,transparent,var(--acc),transparent);}
|
| 708 |
+
#logo{display:flex;flex-direction:column;gap:1px;flex-shrink:0;}
|
| 709 |
+
#logo-main{font-size:1.3rem;font-weight:800;letter-spacing:4px;
|
| 710 |
+
background:linear-gradient(90deg,var(--acc),var(--pulse),var(--acc2));
|
| 711 |
+
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
|
| 712 |
+
#logo-sub{font-size:.45rem;color:var(--sub);letter-spacing:.3em;text-transform:uppercase;}
|
| 713 |
+
#pulse-dot{width:10px;height:10px;border-radius:50%;background:var(--pulse);flex-shrink:0;
|
| 714 |
+
box-shadow:0 0 12px var(--pulse);animation:pulsebeat 2s ease-in-out infinite;}
|
| 715 |
+
@keyframes pulsebeat{0%,100%{transform:scale(1);opacity:1;}50%{transform:scale(1.4);opacity:.6;}}
|
| 716 |
+
#hdr-center{flex:1;display:flex;gap:.4rem;flex-wrap:wrap;align-items:center;}
|
| 717 |
+
.hs{display:flex;align-items:center;gap:.3rem;background:var(--s1);border:1px solid var(--bd);
|
| 718 |
+
border-radius:5px;padding:.22rem .55rem;font-size:.52rem;color:var(--sub);}
|
| 719 |
+
.hs-n{font-size:.85rem;font-weight:700;color:var(--txt);line-height:1;}
|
| 720 |
+
#nav{display:flex;gap:.2rem;flex-shrink:0;}
|
| 721 |
+
.nav-btn{background:transparent;border:1px solid var(--bd);color:var(--sub);
|
| 722 |
+
padding:.32rem .75rem;font-family:var(--font);font-size:.6rem;font-weight:600;
|
| 723 |
+
border-radius:5px;cursor:pointer;transition:all .12s;letter-spacing:.08em;}
|
| 724 |
+
.nav-btn:hover{border-color:var(--bd2);color:var(--txt);}
|
| 725 |
+
.nav-btn.on{background:var(--acc);color:#000;border-color:var(--acc);}
|
| 726 |
+
|
| 727 |
+
/* LAYOUT */
|
| 728 |
+
#layout{flex:1;display:flex;min-height:0;overflow:hidden;}
|
| 729 |
+
#sidebar{width:220px;flex-shrink:0;border-right:1px solid var(--bd);
|
| 730 |
+
display:flex;flex-direction:column;overflow:hidden;background:var(--s1);}
|
| 731 |
+
#main-area{flex:1;overflow:hidden;display:flex;flex-direction:column;}
|
| 732 |
+
|
| 733 |
+
/* SIDEBAR */
|
| 734 |
+
.sb-section{padding:.5rem .7rem .3rem;font-size:.44rem;font-weight:700;letter-spacing:.18em;
|
| 735 |
+
color:var(--sub);text-transform:uppercase;border-top:1px solid var(--bd);margin-top:.3rem;}
|
| 736 |
+
.sb-section:first-child{border-top:none;margin-top:0;}
|
| 737 |
+
.agent-card{margin:.2rem .5rem;border-radius:7px;border:1px solid var(--bd);
|
| 738 |
+
background:var(--s2);overflow:hidden;cursor:pointer;transition:all .12s;}
|
| 739 |
+
.agent-card:hover{border-color:var(--bd2);}
|
| 740 |
+
.agent-card.active{border-color:var(--acc);}
|
| 741 |
+
.ac-header{display:flex;align-items:center;gap:.4rem;padding:.38rem .5rem;}
|
| 742 |
+
.ac-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;}
|
| 743 |
+
.ac-dot.running{animation:pulsebeat 1s ease-in-out infinite;}
|
| 744 |
+
.ac-name{font-size:.65rem;font-weight:700;flex:1;}
|
| 745 |
+
.ac-hb{font-size:.45rem;color:var(--sub);}
|
| 746 |
+
.ac-status{font-size:.45rem;padding:.15rem .38rem;border-radius:3px;}
|
| 747 |
+
.ac-status.idle{background:rgba(255,107,0,.08);color:var(--acc);}
|
| 748 |
+
.ac-status.running{background:rgba(57,255,106,.12);color:var(--lo);}
|
| 749 |
+
.ac-status.error{background:rgba(255,34,85,.1);color:var(--cr);}
|
| 750 |
+
.ac-result{font-size:.5rem;color:var(--sub);padding:0 .5rem .38rem;
|
| 751 |
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;}
|
| 752 |
+
.ac-actions{display:flex;gap:.2rem;padding:0 .5rem .38rem;}
|
| 753 |
+
.ac-btn{font-size:.48rem;background:var(--s1);border:1px solid var(--bd);border-radius:3px;
|
| 754 |
+
padding:1px 6px;cursor:pointer;color:var(--sub);font-family:var(--font);}
|
| 755 |
+
.ac-btn:hover{color:var(--lo);border-color:var(--lo);}
|
| 756 |
+
.sb-scroll{flex:1;overflow-y:auto;padding:.3rem 0;}
|
| 757 |
+
.sb-scroll::-webkit-scrollbar{width:2px;}
|
| 758 |
+
.sb-scroll::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 759 |
+
|
| 760 |
+
/* SPACES HEALTH */
|
| 761 |
+
.space-row{display:flex;align-items:center;gap:.4rem;padding:.22rem .65rem;font-size:.55rem;}
|
| 762 |
+
.sp-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
|
| 763 |
+
.sp-dot.ok{background:var(--lo);}
|
| 764 |
+
.sp-dot.bad{background:var(--cr);}
|
| 765 |
+
.sp-dot.unk{background:var(--sub);}
|
| 766 |
+
.sp-name{flex:1;color:var(--sub);}
|
| 767 |
+
.sp-status{font-size:.45rem;color:var(--dim);}
|
| 768 |
+
|
| 769 |
+
/* PANELS */
|
| 770 |
+
.panel{display:none;flex:1;overflow:hidden;flex-direction:column;}
|
| 771 |
+
.panel.on{display:flex;}
|
| 772 |
+
|
| 773 |
+
/* TIMETABLE */
|
| 774 |
+
#panel-timetable{padding:0;}
|
| 775 |
+
#cal-header{flex-shrink:0;display:flex;align-items:center;gap:.8rem;
|
| 776 |
+
padding:.6rem 1.2rem;border-bottom:1px solid var(--bd);background:var(--s1);}
|
| 777 |
+
#cal-title{font-size:.9rem;font-weight:700;letter-spacing:.08em;}
|
| 778 |
+
.cal-nav{background:var(--s2);border:1px solid var(--bd);color:var(--sub);
|
| 779 |
+
width:28px;height:28px;border-radius:5px;display:flex;align-items:center;justify-content:center;
|
| 780 |
+
cursor:pointer;font-size:.8rem;}
|
| 781 |
+
.cal-nav:hover{border-color:var(--acc);color:var(--acc);}
|
| 782 |
+
#btn-add-sched{background:var(--acc);color:#000;border:none;padding:.32rem .75rem;
|
| 783 |
+
font-family:var(--font);font-size:.6rem;font-weight:700;letter-spacing:.08em;
|
| 784 |
+
border-radius:5px;cursor:pointer;margin-left:auto;}
|
| 785 |
+
#btn-add-sched:hover{background:var(--acc2);}
|
| 786 |
+
#cal-body{flex:1;overflow:hidden;display:flex;flex-direction:column;}
|
| 787 |
+
#cal-days-hdr{display:grid;grid-template-columns:var(--mon-w) repeat(7,1fr);
|
| 788 |
+
border-bottom:1px solid var(--bd);flex-shrink:0;}
|
| 789 |
+
.cal-day-hdr{padding:.35rem .5rem;font-size:.55rem;font-weight:600;text-align:center;
|
| 790 |
+
color:var(--sub);border-left:1px solid var(--bd);}
|
| 791 |
+
.cal-day-hdr.today{color:var(--acc);}
|
| 792 |
+
.cal-day-hdr:first-child{border-left:none;font-size:.45rem;color:var(--dim);}
|
| 793 |
+
#cal-grid{flex:1;overflow-y:auto;position:relative;}
|
| 794 |
+
#cal-grid::-webkit-scrollbar{width:4px;}
|
| 795 |
+
#cal-grid::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 796 |
+
.cal-row{display:grid;grid-template-columns:var(--mon-w) repeat(7,1fr);
|
| 797 |
+
border-bottom:1px solid var(--bd);min-height:52px;}
|
| 798 |
+
.cal-time-cell{padding:.3rem .5rem;font-size:.5rem;color:var(--sub);font-family:var(--mono);
|
| 799 |
+
border-right:1px solid var(--bd);display:flex;align-items:flex-start;justify-content:flex-end;
|
| 800 |
+
padding-top:.6rem;}
|
| 801 |
+
.cal-cell{border-left:1px solid var(--bd);padding:.2rem .18rem;position:relative;min-height:52px;}
|
| 802 |
+
.cal-cell.today{background:rgba(255,107,0,.02);}
|
| 803 |
+
.cal-event{border-radius:5px;padding:.22rem .38rem;margin:.08rem 0;cursor:pointer;
|
| 804 |
+
font-size:.52rem;font-weight:600;border-left:3px solid;transition:all .1s;
|
| 805 |
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:.3rem;}
|
| 806 |
+
.cal-event:hover{opacity:.85;transform:translateX(2px);}
|
| 807 |
+
.cal-event .ev-agent{font-size:.42rem;opacity:.75;font-weight:400;}
|
| 808 |
+
.ev-new{animation:evslide .2s ease;}
|
| 809 |
+
@keyframes evslide{from{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:none}}
|
| 810 |
+
|
| 811 |
+
/* AGENTS PANEL */
|
| 812 |
+
#panel-agents{padding:.8rem 1.2rem;overflow-y:auto;}
|
| 813 |
+
#panel-agents::-webkit-scrollbar{width:4px;}
|
| 814 |
+
#panel-agents::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 815 |
+
.agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:.75rem;}
|
| 816 |
+
.agent-full-card{background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:1rem;
|
| 817 |
+
transition:border-color .12s;}
|
| 818 |
+
.agent-full-card:hover{border-color:var(--bd2);}
|
| 819 |
+
.afc-header{display:flex;align-items:center;gap:.6rem;margin-bottom:.7rem;}
|
| 820 |
+
.afc-dot{width:12px;height:12px;border-radius:50%;}
|
| 821 |
+
.afc-name{font-size:.95rem;font-weight:700;flex:1;}
|
| 822 |
+
.afc-toggle{background:var(--s2);border:1px solid var(--bd);border-radius:4px;padding:.24rem .55rem;
|
| 823 |
+
font-size:.52rem;cursor:pointer;font-family:var(--font);}
|
| 824 |
+
.afc-toggle.on{border-color:var(--lo);color:var(--lo);}
|
| 825 |
+
.afc-toggle.off{border-color:var(--cr);color:var(--cr);}
|
| 826 |
+
.afc-persona{font-size:.58rem;color:var(--sub);line-height:1.5;margin-bottom:.7rem;
|
| 827 |
+
max-height:60px;overflow:hidden;text-overflow:ellipsis;}
|
| 828 |
+
.afc-meta{display:flex;gap:.35rem;flex-wrap:wrap;margin-bottom:.7rem;}
|
| 829 |
+
.afc-tag{font-size:.48rem;background:var(--s2);border:1px solid var(--bd);border-radius:3px;
|
| 830 |
+
padding:1px 6px;color:var(--sub);}
|
| 831 |
+
.afc-actions{display:flex;gap:.35rem;}
|
| 832 |
+
.afc-btn{font-size:.55rem;background:var(--s2);border:1px solid var(--bd);border-radius:4px;
|
| 833 |
+
padding:.26rem .6rem;cursor:pointer;font-family:var(--font);color:var(--sub);}
|
| 834 |
+
.afc-btn:hover{border-color:var(--acc);color:var(--acc);}
|
| 835 |
+
.afc-btn.run{border-color:rgba(57,255,106,.3);color:var(--lo);background:rgba(57,255,106,.04);}
|
| 836 |
+
.afc-btn.run:hover{border-color:var(--lo);background:rgba(57,255,106,.1);}
|
| 837 |
+
#btn-add-agent{background:var(--acc);color:#000;border:none;padding:.38rem .9rem;
|
| 838 |
+
font-family:var(--font);font-size:.62rem;font-weight:700;border-radius:5px;
|
| 839 |
+
cursor:pointer;margin-bottom:.8rem;letter-spacing:.06em;}
|
| 840 |
+
|
| 841 |
+
/* LIVE FEED */
|
| 842 |
+
#panel-live{padding:0;overflow:hidden;}
|
| 843 |
+
#live-wrap{display:flex;height:100%;}
|
| 844 |
+
#live-feed{flex:1;overflow-y:auto;padding:.6rem .9rem;font-family:var(--mono);font-size:.65rem;}
|
| 845 |
+
#live-feed::-webkit-scrollbar{width:4px;}
|
| 846 |
+
#live-feed::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 847 |
+
.lf-item{display:flex;align-items:flex-start;gap:.55rem;padding:.32rem .45rem;
|
| 848 |
+
border-radius:5px;margin-bottom:.22rem;animation:lfin .18s ease;}
|
| 849 |
+
@keyframes lfin{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
|
| 850 |
+
.lf-icon{font-size:.8rem;flex-shrink:0;width:18px;text-align:center;}
|
| 851 |
+
.lf-ts{font-size:.48rem;color:var(--dim);flex-shrink:0;width:52px;padding-top:2px;}
|
| 852 |
+
.lf-body{flex:1;}
|
| 853 |
+
.lf-agent{font-size:.6rem;font-weight:700;margin-right:.35rem;}
|
| 854 |
+
.lf-msg{font-size:.6rem;color:var(--sub);}
|
| 855 |
+
.lf-type-heartbeat{background:rgba(240,192,64,.03);}
|
| 856 |
+
.lf-type-react_start{background:rgba(56,189,248,.04);}
|
| 857 |
+
.lf-type-react_step{background:rgba(139,92,246,.04);}
|
| 858 |
+
.lf-type-react_done{background:rgba(57,255,106,.04);}
|
| 859 |
+
.lf-type-error{background:rgba(255,34,85,.06);}
|
| 860 |
+
.lf-type-idle{opacity:.4;}
|
| 861 |
+
#live-sidebar{width:300px;border-left:1px solid var(--bd);display:flex;flex-direction:column;overflow:hidden;}
|
| 862 |
+
#live-sidebar-hdr{padding:.55rem .8rem;border-bottom:1px solid var(--bd);font-size:.55rem;
|
| 863 |
+
font-weight:700;letter-spacing:.12em;color:var(--acc);}
|
| 864 |
+
#trace-list{flex:1;overflow-y:auto;padding:.4rem;}
|
| 865 |
+
#trace-list::-webkit-scrollbar{width:2px;}
|
| 866 |
+
#trace-list::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 867 |
+
.trace-item{background:var(--s2);border:1px solid var(--bd);border-radius:6px;
|
| 868 |
+
padding:.5rem .65rem;margin-bottom:.3rem;cursor:pointer;transition:all .1s;}
|
| 869 |
+
.trace-item:hover{border-color:var(--bd2);}
|
| 870 |
+
.tr-agent{font-size:.62rem;font-weight:700;margin-bottom:.2rem;}
|
| 871 |
+
.tr-result{font-size:.55rem;color:var(--sub);overflow:hidden;text-overflow:ellipsis;
|
| 872 |
+
white-space:nowrap;margin-bottom:.25rem;}
|
| 873 |
+
.tr-meta{display:flex;gap:.4rem;font-size:.48rem;color:var(--dim);}
|
| 874 |
+
.tr-ok{color:var(--lo);}.tr-fail{color:var(--cr);}
|
| 875 |
+
|
| 876 |
+
/* TRACE DETAIL MODAL */
|
| 877 |
+
#trace-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;
|
| 878 |
+
backdrop-filter:blur(6px);align-items:center;justify-content:center;}
|
| 879 |
+
#trace-modal.open{display:flex;}
|
| 880 |
+
.tm-box{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc);
|
| 881 |
+
border-radius:12px;width:750px;max-width:98vw;max-height:90vh;display:flex;flex-direction:column;
|
| 882 |
+
animation:mdin .16s ease;}
|
| 883 |
+
@keyframes mdin{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:none}}
|
| 884 |
+
.tm-hdr{display:flex;align-items:center;gap:.7rem;padding:.8rem 1.1rem;border-bottom:1px solid var(--bd);}
|
| 885 |
+
.tm-title{font-size:.8rem;font-weight:700;flex:1;color:var(--acc);}
|
| 886 |
+
.tm-close{background:none;border:none;color:var(--sub);cursor:pointer;font-size:1rem;
|
| 887 |
+
width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;}
|
| 888 |
+
.tm-close:hover{background:var(--bd2);color:var(--txt);}
|
| 889 |
+
.tm-body{flex:1;overflow-y:auto;padding:.9rem 1.1rem;}
|
| 890 |
+
.tm-body::-webkit-scrollbar{width:4px;}
|
| 891 |
+
.tm-body::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 892 |
+
.step-block{margin-bottom:.9rem;background:var(--s2);border:1px solid var(--bd);border-radius:8px;overflow:hidden;}
|
| 893 |
+
.step-hdr{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;background:var(--s1);
|
| 894 |
+
border-bottom:1px solid var(--bd);}
|
| 895 |
+
.step-n{font-size:.58rem;background:var(--acc);color:#000;border-radius:3px;padding:1px 6px;font-weight:700;}
|
| 896 |
+
.step-action{font-size:.62rem;font-weight:700;font-family:var(--mono);}
|
| 897 |
+
.step-body{padding:.55rem .75rem;}
|
| 898 |
+
.step-label{font-size:.48rem;color:var(--sub);font-weight:700;letter-spacing:.12em;
|
| 899 |
+
text-transform:uppercase;margin-bottom:.18rem;}
|
| 900 |
+
.step-text{font-size:.6rem;color:var(--txt);font-family:var(--mono);white-space:pre-wrap;
|
| 901 |
+
word-break:break-word;line-height:1.55;}
|
| 902 |
+
.step-obs{background:rgba(57,255,106,.04);border-top:1px solid var(--bd);padding:.45rem .75rem;}
|
| 903 |
+
|
| 904 |
+
/* MODAL (agents / schedule) */
|
| 905 |
+
#modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:100;
|
| 906 |
+
backdrop-filter:blur(5px);align-items:center;justify-content:center;}
|
| 907 |
+
#modal.open{display:flex;}
|
| 908 |
+
.mdl{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc);
|
| 909 |
+
border-radius:12px;width:520px;max-width:98vw;max-height:90vh;display:flex;flex-direction:column;
|
| 910 |
+
animation:mdin .16s ease;position:relative;}
|
| 911 |
+
.mdl-hdr{padding:.85rem 1.1rem;border-bottom:1px solid var(--bd);display:flex;align-items:center;}
|
| 912 |
+
.mdl-title{font-size:.75rem;font-weight:700;letter-spacing:2px;flex:1;color:var(--acc);}
|
| 913 |
+
.mdl-x{background:none;border:none;color:var(--sub);cursor:pointer;font-size:.9rem;
|
| 914 |
+
width:26px;height:26px;border-radius:4px;display:flex;align-items:center;justify-content:center;}
|
| 915 |
+
.mdl-x:hover{background:var(--bd2);}
|
| 916 |
+
.mdl-body{flex:1;overflow-y:auto;padding:1rem 1.1rem;}
|
| 917 |
+
.mdl-body::-webkit-scrollbar{width:3px;}
|
| 918 |
+
.mdl-body::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 919 |
+
.mfl{margin-bottom:.7rem;}
|
| 920 |
+
.mfl label{display:block;font-size:.46rem;color:var(--sub);text-transform:uppercase;
|
| 921 |
+
letter-spacing:.12em;margin-bottom:.22rem;font-weight:700;}
|
| 922 |
+
.mfl input,.mfl select,.mfl textarea{width:100%;background:var(--s2);border:1px solid var(--bd2);
|
| 923 |
+
border-radius:5px;padding:.38rem .55rem;font-family:var(--font);font-size:.65rem;color:var(--txt);
|
| 924 |
+
outline:none;transition:border-color .12s;}
|
| 925 |
+
.mfl input:focus,.mfl select:focus,.mfl textarea:focus{border-color:var(--acc);}
|
| 926 |
+
.mfl textarea{resize:vertical;min-height:80px;font-family:var(--mono);}
|
| 927 |
+
.mfl-row{display:grid;grid-template-columns:1fr 1fr;gap:.5rem;}
|
| 928 |
+
.mfl-row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.5rem;}
|
| 929 |
+
.mdl-footer{padding:.75rem 1.1rem;border-top:1px solid var(--bd);display:flex;gap:.4rem;}
|
| 930 |
+
.mdl-ok{flex:1;background:var(--acc);color:#000;border:none;padding:.44rem;
|
| 931 |
+
font-family:var(--font);font-size:.65rem;font-weight:700;letter-spacing:.1em;
|
| 932 |
+
text-transform:uppercase;border-radius:5px;cursor:pointer;}
|
| 933 |
+
.mdl-ok:hover{background:var(--acc2);}
|
| 934 |
+
.mdl-cancel{background:var(--s2);color:var(--sub);border:1px solid var(--bd2);
|
| 935 |
+
padding:.44rem .9rem;font-family:var(--font);font-size:.65rem;border-radius:5px;cursor:pointer;}
|
| 936 |
+
.mdl-cancel:hover{color:var(--txt);}
|
| 937 |
+
|
| 938 |
+
/* TOAST */
|
| 939 |
+
#toasts{position:fixed;bottom:1rem;right:1rem;z-index:300;display:flex;flex-direction:column;gap:.3rem;}
|
| 940 |
+
.tst{background:var(--s1);border:1px solid var(--bd2);border-left:3px solid var(--acc);
|
| 941 |
+
padding:.38rem .72rem;font-size:.58rem;border-radius:5px;animation:tin .14s ease;color:var(--txt);}
|
| 942 |
+
.tst.ok{border-left-color:var(--lo);}.tst.err{border-left-color:var(--cr);}
|
| 943 |
+
.tst.warn{border-left-color:var(--pulse);}
|
| 944 |
+
@keyframes tin{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}
|
| 945 |
+
|
| 946 |
+
/* SPACES OVERVIEW */
|
| 947 |
+
#panel-spaces{padding:.8rem 1.2rem;overflow-y:auto;}
|
| 948 |
+
#panel-spaces::-webkit-scrollbar{width:4px;}
|
| 949 |
+
#panel-spaces::-webkit-scrollbar-thumb{background:var(--bd2);}
|
| 950 |
+
.spaces-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.65rem;}
|
| 951 |
+
.space-card{background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:.85rem 1rem;
|
| 952 |
+
transition:all .12s;}
|
| 953 |
+
.space-card:hover{border-color:var(--bd2);}
|
| 954 |
+
.sc-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;}
|
| 955 |
+
.sc-dot{width:10px;height:10px;border-radius:50%;}
|
| 956 |
+
.sc-dot.ok{background:var(--lo);box-shadow:0 0 8px var(--lo);}
|
| 957 |
+
.sc-dot.bad{background:var(--cr);}
|
| 958 |
+
.sc-dot.unk{background:var(--sub);animation:pulsebeat 2s ease-in-out infinite;}
|
| 959 |
+
.sc-name{font-size:.78rem;font-weight:700;flex:1;}
|
| 960 |
+
.sc-desc{font-size:.55rem;color:var(--sub);line-height:1.5;margin-bottom:.5rem;}
|
| 961 |
+
.sc-tools{display:flex;flex-wrap:wrap;gap:.2rem;margin-bottom:.5rem;}
|
| 962 |
+
.sc-tool{font-size:.44rem;background:var(--s2);border:1px solid var(--bd);border-radius:3px;
|
| 963 |
+
padding:1px 5px;color:var(--dim);}
|
| 964 |
+
.sc-url{font-size:.48rem;color:var(--acc);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;}
|
| 965 |
+
.sc-link{text-decoration:none;font-size:.5rem;background:var(--s2);border:1px solid var(--bd);
|
| 966 |
+
border-radius:3px;padding:2px 7px;color:var(--sub);margin-top:.4rem;display:inline-block;}
|
| 967 |
+
.sc-link:hover{color:var(--acc);border-color:var(--acc);}
|
| 968 |
+
|
| 969 |
+
/* Flashing run indicator */
|
| 970 |
+
@keyframes runglow{0%,100%{box-shadow:0 0 0 0 transparent}50%{box-shadow:0 0 12px 2px var(--lo)}}
|
| 971 |
+
.running-glow{animation:runglow 1.2s ease-in-out infinite;}
|
| 972 |
+
</style>
|
| 973 |
+
</head>
|
| 974 |
+
<body>
|
| 975 |
+
<div id="hdr">
|
| 976 |
+
<span id="pulse-dot"></span>
|
| 977 |
+
<div id="logo">
|
| 978 |
+
<div id="logo-main">PULSE</div>
|
| 979 |
+
<div id="logo-sub">Agent Nervous System — ki-fusion-labs.de</div>
|
| 980 |
+
</div>
|
| 981 |
+
<div id="hdr-center">
|
| 982 |
+
<div class="hs"><span class="hs-n" id="hs-agents">0</span>AGENTS</div>
|
| 983 |
+
<div class="hs"><span class="hs-n" id="hs-running">0</span>RUNNING</div>
|
| 984 |
+
<div class="hs"><span class="hs-n" id="hs-jobs">0</span>SCHEDULED</div>
|
| 985 |
+
<div class="hs"><span class="hs-n" id="hs-ticks">0</span>TICKS TODAY</div>
|
| 986 |
+
</div>
|
| 987 |
+
<div id="nav">
|
| 988 |
+
<button class="nav-btn on" data-panel="timetable">📅 Timetable</button>
|
| 989 |
+
<button class="nav-btn" data-panel="agents">🤖 Agents</button>
|
| 990 |
+
<button class="nav-btn" data-panel="live">⚡ Live</button>
|
| 991 |
+
<button class="nav-btn" data-panel="spaces">🌐 Spaces</button>
|
| 992 |
+
</div>
|
| 993 |
+
</div>
|
| 994 |
+
|
| 995 |
+
<div id="layout">
|
| 996 |
+
<div id="sidebar">
|
| 997 |
+
<div class="sb-scroll">
|
| 998 |
+
<div class="sb-section">Active Agents</div>
|
| 999 |
+
<div id="agent-list"></div>
|
| 1000 |
+
<div class="sb-section" style="margin-top:.5rem">Connected Spaces</div>
|
| 1001 |
+
<div id="space-health-list"></div>
|
| 1002 |
+
</div>
|
| 1003 |
+
</div>
|
| 1004 |
+
|
| 1005 |
+
<div id="main-area">
|
| 1006 |
+
|
| 1007 |
+
<!-- TIMETABLE -->
|
| 1008 |
+
<div id="panel-timetable" class="panel on">
|
| 1009 |
+
<div id="cal-header">
|
| 1010 |
+
<button class="cal-nav" id="btn-prev-week">‹</button>
|
| 1011 |
+
<div id="cal-title">Week</div>
|
| 1012 |
+
<button class="cal-nav" id="btn-next-week">›</button>
|
| 1013 |
+
<button class="cal-nav" id="btn-today" title="Go to today" style="font-size:.55rem;width:auto;padding:0 .5rem">Today</button>
|
| 1014 |
+
<button id="btn-add-sched">+ Schedule</button>
|
| 1015 |
+
</div>
|
| 1016 |
+
<div id="cal-body">
|
| 1017 |
+
<div id="cal-days-hdr">
|
| 1018 |
+
<div class="cal-day-hdr" style="border-left:none">UTC</div>
|
| 1019 |
+
</div>
|
| 1020 |
+
<div id="cal-grid"></div>
|
| 1021 |
+
</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
|
| 1024 |
+
<!-- AGENTS -->
|
| 1025 |
+
<div id="panel-agents" class="panel">
|
| 1026 |
+
<button id="btn-add-agent">+ New Agent</button>
|
| 1027 |
+
<div class="agents-grid" id="agents-full-grid"></div>
|
| 1028 |
+
</div>
|
| 1029 |
+
|
| 1030 |
+
<!-- LIVE -->
|
| 1031 |
+
<div id="panel-live" class="panel">
|
| 1032 |
+
<div id="live-wrap">
|
| 1033 |
+
<div id="live-feed"></div>
|
| 1034 |
+
<div id="live-sidebar">
|
| 1035 |
+
<div id="live-sidebar-hdr">RECENT TRACES</div>
|
| 1036 |
+
<div id="trace-list"></div>
|
| 1037 |
+
</div>
|
| 1038 |
+
</div>
|
| 1039 |
+
</div>
|
| 1040 |
+
|
| 1041 |
+
<!-- SPACES -->
|
| 1042 |
+
<div id="panel-spaces" class="panel">
|
| 1043 |
+
<div class="spaces-grid" id="spaces-grid"></div>
|
| 1044 |
+
</div>
|
| 1045 |
+
|
| 1046 |
+
</div>
|
| 1047 |
+
</div>
|
| 1048 |
+
|
| 1049 |
+
<!-- TRACE MODAL -->
|
| 1050 |
+
<div id="trace-modal">
|
| 1051 |
+
<div class="tm-box">
|
| 1052 |
+
<div class="tm-hdr">
|
| 1053 |
+
<span class="tm-title" id="tm-title">TRACE</span>
|
| 1054 |
+
<button class="tm-close" id="tm-close">✕</button>
|
| 1055 |
+
</div>
|
| 1056 |
+
<div class="tm-body" id="tm-body"></div>
|
| 1057 |
+
</div>
|
| 1058 |
+
</div>
|
| 1059 |
+
|
| 1060 |
+
<!-- MODAL -->
|
| 1061 |
+
<div id="modal">
|
| 1062 |
+
<div class="mdl">
|
| 1063 |
+
<div class="mdl-hdr">
|
| 1064 |
+
<span class="mdl-title" id="mdl-title">MODAL</span>
|
| 1065 |
+
<button class="mdl-x" id="mdl-x">✕</button>
|
| 1066 |
+
</div>
|
| 1067 |
+
<div class="mdl-body" id="mdl-body"></div>
|
| 1068 |
+
<div class="mdl-footer">
|
| 1069 |
+
<button class="mdl-ok" id="mdl-ok">Save</button>
|
| 1070 |
+
<button class="mdl-cancel" id="mdl-cancel">Cancel</button>
|
| 1071 |
+
</div>
|
| 1072 |
+
</div>
|
| 1073 |
+
</div>
|
| 1074 |
+
|
| 1075 |
+
<div id="toasts"></div>
|
| 1076 |
+
|
| 1077 |
+
<script>
|
| 1078 |
+
// ββ Utils ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1079 |
+
var S = function(id){return document.getElementById(id);};
|
| 1080 |
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
| 1081 |
+
function post(u,d){return fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});}
|
| 1082 |
+
function del(u,d){return fetch(u,{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify(d||{})});}
|
| 1083 |
+
function toast(msg,t){var e=document.createElement('div');e.className='tst'+(t?' '+t:'');e.textContent=msg;S('toasts').appendChild(e);setTimeout(function(){e.remove();},2600);}
|
| 1084 |
+
function fmtTime(ts){return new Date(ts*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}
|
| 1085 |
+
function fmtDateTime(ts){return new Date(ts*1000).toLocaleString([],{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});}
|
| 1086 |
+
|
| 1087 |
+
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1088 |
+
var AGENTS = [], SCHEDULE = [], SPACES_HEALTH = {};
|
| 1089 |
+
var WEEK_START = getMonday(new Date());
|
| 1090 |
+
var SELECTED_AGENT = null;
|
| 1091 |
+
var MODAL_MODE = '', MODAL_DATA = {};
|
| 1092 |
+
var TICKS_TODAY = 0;
|
| 1093 |
+
|
| 1094 |
+
function getMonday(d){
|
| 1095 |
+
var dd=new Date(d); var day=dd.getDay(); var diff=dd.getDate()-day+(day==0?-6:1);
|
| 1096 |
+
dd.setDate(diff); dd.setHours(0,0,0,0); return dd;
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
// ββ Panel nav βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1100 |
+
document.querySelectorAll('.nav-btn[data-panel]').forEach(function(btn){
|
| 1101 |
+
btn.addEventListener('click',function(){
|
| 1102 |
+
document.querySelectorAll('.nav-btn').forEach(function(b){b.classList.remove('on');});
|
| 1103 |
+
document.querySelectorAll('.panel').forEach(function(p){p.classList.remove('on');});
|
| 1104 |
+
this.classList.add('on');
|
| 1105 |
+
S('panel-'+this.getAttribute('data-panel')).classList.add('on');
|
| 1106 |
+
if(this.getAttribute('data-panel')==='live') loadTraces();
|
| 1107 |
+
if(this.getAttribute('data-panel')==='spaces') renderSpacesPanel();
|
| 1108 |
+
});
|
| 1109 |
+
});
|
| 1110 |
+
|
| 1111 |
+
// ββ Load agents βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1112 |
+
function loadAgents(){
|
| 1113 |
+
fetch('/api/agents').then(function(r){return r.json();}).then(function(data){
|
| 1114 |
+
AGENTS=data;
|
| 1115 |
+
renderSidebarAgents();
|
| 1116 |
+
renderAgentsFull();
|
| 1117 |
+
S('hs-agents').textContent=data.length;
|
| 1118 |
+
var running=data.filter(function(a){return (a.status||{}).running;}).length;
|
| 1119 |
+
S('hs-running').textContent=running;
|
| 1120 |
+
});
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
function agentColor(a){return a.color||'#ff6b00';}
|
| 1124 |
+
function agentStatusClass(a){
|
| 1125 |
+
var st=a.status||{};
|
| 1126 |
+
if(st.running) return 'running';
|
| 1127 |
+
if((st.last_ok===false)&&st.last_result) return 'error';
|
| 1128 |
+
return 'idle';
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
function renderSidebarAgents(){
|
| 1132 |
+
var list=S('agent-list'); list.innerHTML='';
|
| 1133 |
+
AGENTS.forEach(function(a){
|
| 1134 |
+
var st=a.status||{}; var sc=agentStatusClass(a);
|
| 1135 |
+
var div=document.createElement('div');
|
| 1136 |
+
div.className='agent-card'+(SELECTED_AGENT===a.name?' active':'');
|
| 1137 |
+
div.innerHTML=
|
| 1138 |
+
'<div class="ac-header">'
|
| 1139 |
+
+'<div class="ac-dot '+sc+(sc==='running'?' running-glow':'')+'" style="background:'+agentColor(a)+'"></div>'
|
| 1140 |
+
+'<span class="ac-name">'+esc(a.name)+'</span>'
|
| 1141 |
+
+'<span class="ac-status '+sc+'">'+sc+'</span>'
|
| 1142 |
+
+'</div>'
|
| 1143 |
+
+(st.last_result?'<div class="ac-result" title="'+esc(st.last_result)+'">'+esc(st.last_result)+'</div>':'')
|
| 1144 |
+
+'<div class="ac-actions">'
|
| 1145 |
+
+'<button class="ac-btn" data-name="'+esc(a.name)+'" data-action="run">► Run</button>'
|
| 1146 |
+
+'<button class="ac-btn" data-name="'+esc(a.name)+'" data-action="edit">Edit</button>'
|
| 1147 |
+
+'</div>';
|
| 1148 |
+
list.appendChild(div);
|
| 1149 |
+
div.querySelectorAll('[data-action]').forEach(function(btn){
|
| 1150 |
+
btn.addEventListener('click',function(e){e.stopPropagation();
|
| 1151 |
+
var n=this.getAttribute('data-name'), ac=this.getAttribute('data-action');
|
| 1152 |
+
if(ac==='run') triggerAgent(n,'manual','Manual trigger from UI');
|
| 1153 |
+
else openAgentModal(n);
|
| 1154 |
+
});
|
| 1155 |
+
});
|
| 1156 |
+
div.addEventListener('click',function(){SELECTED_AGENT=a.name;renderSidebarAgents();});
|
| 1157 |
+
});
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
function renderAgentsFull(){
|
| 1161 |
+
var grid=S('agents-full-grid'); grid.innerHTML='';
|
| 1162 |
+
AGENTS.forEach(function(a){
|
| 1163 |
+
var st=a.status||{}; var en=a.enabled!==false;
|
| 1164 |
+
var div=document.createElement('div'); div.className='agent-full-card';
|
| 1165 |
+
div.innerHTML=
|
| 1166 |
+
'<div class="afc-header">'
|
| 1167 |
+
+'<div class="afc-dot" style="background:'+agentColor(a)+';box-shadow:0 0 10px '+agentColor(a)+'44"></div>'
|
| 1168 |
+
+'<span class="afc-name">'+esc(a.name)+'</span>'
|
| 1169 |
+
+'<button class="afc-toggle '+(en?'on':'off')+'" data-name="'+esc(a.name)+'">'+(en?'✔ ON':'✖ OFF')+'</button>'
|
| 1170 |
+
+'</div>'
|
| 1171 |
+
+'<div class="afc-persona">'+esc(a.persona||'')+'</div>'
|
| 1172 |
+
+'<div class="afc-meta">'
|
| 1173 |
+
+(a.heartbeat_seconds>0?'<span class="afc-tag">♥ every '+a.heartbeat_seconds+'s</span>':'')
|
| 1174 |
+
+'<span class="afc-tag">'+esc(a.cost_mode||'balanced')+'</span>'
|
| 1175 |
+
+'<span class="afc-tag">max '+a.max_react_steps+' steps</span>'
|
| 1176 |
+
+((a.tags||[]).map(function(t){return '<span class="afc-tag">'+esc(t)+'</span>';}).join(''))
|
| 1177 |
+
+'</div>'
|
| 1178 |
+
+(st.last_result?'<div style="font-size:.52rem;color:var(--sub);margin-bottom:.5rem;font-family:var(--mono)">Last: '+esc(st.last_result.substring(0,80))+'</div>':'')
|
| 1179 |
+
+'<div class="afc-actions">'
|
| 1180 |
+
+'<button class="afc-btn run" data-name="'+esc(a.name)+'" data-action="run">► Run Now</button>'
|
| 1181 |
+
+'<button class="afc-btn" data-name="'+esc(a.name)+'" data-action="edit">✏ Edit</button>'
|
| 1182 |
+
+'<button class="afc-btn" data-name="'+esc(a.name)+'" data-action="confer" style="border-color:rgba(139,92,246,.3);color:var(--violet)">💬 Confer</button>'
|
| 1183 |
+
+'<button class="afc-btn" data-name="'+esc(a.name)+'" data-action="delete" style="border-color:rgba(255,34,85,.2);color:var(--cr)">🗑</button>'
|
| 1184 |
+
+'</div>';
|
| 1185 |
+
grid.appendChild(div);
|
| 1186 |
+
div.querySelectorAll('[data-action]').forEach(function(btn){
|
| 1187 |
+
btn.addEventListener('click',function(e){e.stopPropagation();
|
| 1188 |
+
var n=this.getAttribute('data-name'), ac=this.getAttribute('data-action');
|
| 1189 |
+
if(ac==='run') triggerAgent(n,'manual','Manual trigger from Agents panel');
|
| 1190 |
+
else if(ac==='edit') openAgentModal(n);
|
| 1191 |
+
else if(ac==='confer') openConferModal(n);
|
| 1192 |
+
else if(ac==='delete'){if(confirm('Delete agent '+n+'?')) deleteAgent(n);}
|
| 1193 |
+
});
|
| 1194 |
+
});
|
| 1195 |
+
div.querySelector('.afc-toggle').addEventListener('click',function(e){e.stopPropagation();
|
| 1196 |
+
var n=this.getAttribute('data-name'), a2=AGENTS.find(function(x){return x.name===n;});
|
| 1197 |
+
if(a2){a2.enabled=!(a2.enabled!==false);post('/api/agents',a2).then(function(){loadAgents();});}
|
| 1198 |
+
});
|
| 1199 |
+
});
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
function triggerAgent(name, trigger, content){
|
| 1203 |
+
post('/api/agents/'+name+'/run',{trigger:trigger||'manual',content:content||''})
|
| 1204 |
+
.then(function(){toast('Triggered: '+name,'ok');setTimeout(loadAgents,800);});
|
| 1205 |
+
}
|
| 1206 |
+
function deleteAgent(name){
|
| 1207 |
+
del('/api/agents/'+name).then(function(){toast('Deleted: '+name);loadAgents();});
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
// ββ Calendar ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1211 |
+
var HOURS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23];
|
| 1212 |
+
var DAYS = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
| 1213 |
+
var DAY_MAP = {0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'};
|
| 1214 |
+
|
| 1215 |
+
function loadSchedule(){
|
| 1216 |
+
fetch('/api/schedule').then(function(r){return r.json();}).then(function(data){
|
| 1217 |
+
SCHEDULE=data; renderCalendar(); S('hs-jobs').textContent=data.filter(function(e){return e.enabled;}).length;
|
| 1218 |
+
});
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
function renderCalendar(){
|
| 1222 |
+
// Day headers
|
| 1223 |
+
var hdr=S('cal-days-hdr');
|
| 1224 |
+
var weekDates=getWeekDates(WEEK_START);
|
| 1225 |
+
var today=new Date(); today.setHours(0,0,0,0);
|
| 1226 |
+
hdr.innerHTML='<div class="cal-day-hdr" style="border-left:none">UTC</div>';
|
| 1227 |
+
weekDates.forEach(function(d,i){
|
| 1228 |
+
var isToday=d.getTime()===today.getTime();
|
| 1229 |
+
hdr.innerHTML+='<div class="cal-day-hdr'+(isToday?' today':'')+'">'+DAYS[i]+'<br>'
|
| 1230 |
+
+'<span style="font-size:.7rem;font-weight:700;color:'+(isToday?'var(--acc)':'var(--txt)')+'">'+d.getDate()+'</span></div>';
|
| 1231 |
+
});
|
| 1232 |
+
// Update title
|
| 1233 |
+
var y=WEEK_START.getFullYear();
|
| 1234 |
+
var m1=WEEK_START.toLocaleString('default',{month:'short'});
|
| 1235 |
+
var end=new Date(WEEK_START); end.setDate(end.getDate()+6);
|
| 1236 |
+
var m2=end.toLocaleString('default',{month:'short'});
|
| 1237 |
+
S('cal-title').textContent='Week '+getWeekNum(WEEK_START)+' β '+m1+(m2!==m1?' / '+m2:'')+' '+y;
|
| 1238 |
+
// Grid
|
| 1239 |
+
var grid=S('cal-grid'); grid.innerHTML='';
|
| 1240 |
+
HOURS.forEach(function(h){
|
| 1241 |
+
var row=document.createElement('div'); row.className='cal-row';
|
| 1242 |
+
var timeCell=document.createElement('div'); timeCell.className='cal-time-cell';
|
| 1243 |
+
timeCell.textContent=String(h).padStart(2,'0')+':00';
|
| 1244 |
+
row.appendChild(timeCell);
|
| 1245 |
+
weekDates.forEach(function(d,di){
|
| 1246 |
+
var cell=document.createElement('div');
|
| 1247 |
+
var isToday=d.getTime()===today.getTime();
|
| 1248 |
+
cell.className='cal-cell'+(isToday?' today':'');
|
| 1249 |
+
// Find events for this day+hour
|
| 1250 |
+
var dayIdx=di; // 0=Mon
|
| 1251 |
+
SCHEDULE.forEach(function(e){
|
| 1252 |
+
var matches=false;
|
| 1253 |
+
if(e.recurrence==='daily' && e.hour===h) matches=true;
|
| 1254 |
+
if(e.recurrence==='weekly' && e.hour===h && e.day===dayIdx) matches=true;
|
| 1255 |
+
if(e.recurrence==='once'){
|
| 1256 |
+
var dt=e.datetime?new Date(e.datetime):null;
|
| 1257 |
+
if(dt && dt.getDay()===((dayIdx+1)%7) && dt.getHours()===h) matches=true;
|
| 1258 |
+
}
|
| 1259 |
+
if(matches){
|
| 1260 |
+
var ev=document.createElement('div');
|
| 1261 |
+
var col=e.color||'#ff6b00';
|
| 1262 |
+
ev.className='cal-event'; ev.style.borderLeftColor=col;
|
| 1263 |
+
ev.style.background=col+'18'; ev.style.color=col;
|
| 1264 |
+
ev.innerHTML='<span>'+esc(e.title)+'</span><span class="ev-agent">@'+esc(e.agent)+'</span>'
|
| 1265 |
+
+(e.enabled?'':' <span style="opacity:.5">(off)</span>');
|
| 1266 |
+
ev.setAttribute('data-eid',e.id);
|
| 1267 |
+
ev.addEventListener('click',function(evt){evt.stopPropagation();openScheduleModal(e.id);});
|
| 1268 |
+
cell.appendChild(ev);
|
| 1269 |
+
}
|
| 1270 |
+
});
|
| 1271 |
+
row.appendChild(cell);
|
| 1272 |
+
});
|
| 1273 |
+
grid.appendChild(row);
|
| 1274 |
+
});
|
| 1275 |
+
// Scroll to 7am
|
| 1276 |
+
setTimeout(function(){
|
| 1277 |
+
var rows=grid.querySelectorAll('.cal-row');
|
| 1278 |
+
if(rows[7]) rows[7].scrollIntoView({block:'start'});
|
| 1279 |
+
},50);
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
function getWeekDates(monday){
|
| 1283 |
+
var dates=[]; for(var i=0;i<7;i++){var d=new Date(monday);d.setDate(monday.getDate()+i);dates.push(d);} return dates;
|
| 1284 |
+
}
|
| 1285 |
+
function getWeekNum(d){
|
| 1286 |
+
var date=new Date(Date.UTC(d.getFullYear(),d.getMonth(),d.getDate()));
|
| 1287 |
+
var day=date.getUTCDay()||7; date.setUTCDate(date.getUTCDate()+4-day);
|
| 1288 |
+
var yearStart=new Date(Date.UTC(date.getUTCFullYear(),0,1));
|
| 1289 |
+
return Math.ceil((((date-yearStart)/86400000)+1)/7);
|
| 1290 |
+
}
|
| 1291 |
+
|
| 1292 |
+
S('btn-prev-week').addEventListener('click',function(){WEEK_START.setDate(WEEK_START.getDate()-7);renderCalendar();});
|
| 1293 |
+
S('btn-next-week').addEventListener('click',function(){WEEK_START.setDate(WEEK_START.getDate()+7);renderCalendar();});
|
| 1294 |
+
S('btn-today').addEventListener('click',function(){WEEK_START=getMonday(new Date());renderCalendar();});
|
| 1295 |
+
S('btn-add-sched').addEventListener('click',function(){openScheduleModal(null);});
|
| 1296 |
+
|
| 1297 |
+
// ββ Schedule modal βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1298 |
+
function openScheduleModal(eid){
|
| 1299 |
+
var existing=eid?SCHEDULE.find(function(e){return e.id===eid;}):null;
|
| 1300 |
+
MODAL_MODE='schedule'; MODAL_DATA=existing||{};
|
| 1301 |
+
S('mdl-title').textContent=existing?'EDIT SCHEDULE':'ADD SCHEDULE';
|
| 1302 |
+
var agents=AGENTS.map(function(a){return '<option value="'+esc(a.name)+'"'+(existing&&existing.agent===a.name?' selected':'')+'>'+esc(a.name)+'</option>';}).join('');
|
| 1303 |
+
S('mdl-body').innerHTML=
|
| 1304 |
+
'<div class="mfl"><label>Title</label><input id="mi-title" value="'+esc(existing?existing.title:'')+'"></div>'
|
| 1305 |
+
+'<div class="mfl"><label>Agent</label><select id="mi-agent">'+agents+'</select></div>'
|
| 1306 |
+
+'<div class="mfl"><label>Prompt (what the agent should do)</label><textarea id="mi-prompt">'+esc(existing?existing.prompt:'')+'</textarea></div>'
|
| 1307 |
+
+'<div class="mfl-row">'
|
| 1308 |
+
+'<div class="mfl"><label>Recurrence</label><select id="mi-rec">'
|
| 1309 |
+
+'<option value="daily"'+(existing&&existing.recurrence==='daily'?' selected':'')+'>Daily</option>'
|
| 1310 |
+
+'<option value="weekly"'+((!existing||existing.recurrence==='weekly')?' selected':'')+'>Weekly</option>'
|
| 1311 |
+
+'<option value="once"'+(existing&&existing.recurrence==='once'?' selected':'')+'>Once</option>'
|
| 1312 |
+
+'</select></div>'
|
| 1313 |
+
+'<div class="mfl"><label>Day (weekly)</label><select id="mi-day">'
|
| 1314 |
+
+['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(function(d,i){
|
| 1315 |
+
return '<option value="'+i+'"'+(existing&&existing.day===i?' selected':'')+'>'+d+'</option>';}).join('')
|
| 1316 |
+
+'</select></div>'
|
| 1317 |
+
+'</div>'
|
| 1318 |
+
+'<div class="mfl-row">'
|
| 1319 |
+
+'<div class="mfl"><label>Hour (UTC)</label><input id="mi-hour" type="number" min="0" max="23" value="'+(existing?existing.hour:9)+'"></div>'
|
| 1320 |
+
+'<div class="mfl"><label>Minute</label><input id="mi-minute" type="number" min="0" max="59" value="'+(existing?existing.minute:0)+'"></div>'
|
| 1321 |
+
+'</div>'
|
| 1322 |
+
+'<div class="mfl-row">'
|
| 1323 |
+
+'<div class="mfl"><label>Color</label><input id="mi-color" type="color" value="'+(existing&&existing.color?existing.color:'#ff6b00')+'" style="height:36px;padding:2px"></div>'
|
| 1324 |
+
+'<div class="mfl"><label>Enabled</label><select id="mi-enabled">'
|
| 1325 |
+
+'<option value="1"'+((!existing||existing.enabled)?' selected':'')+'>Yes</option>'
|
| 1326 |
+
+'<option value="0"'+(existing&&!existing.enabled?' selected':'')+'>No</option>'
|
| 1327 |
+
+'</select></div>'
|
| 1328 |
+
+'</div>'
|
| 1329 |
+
+(existing?'<div style="margin-top:.5rem;display:flex;gap:.4rem">'
|
| 1330 |
+
+'<button onclick="runScheduleNow(\''+esc(eid)+'\')" style="font-size:.6rem;background:rgba(57,255,106,.08);border:1px solid rgba(57,255,106,.3);color:var(--lo);border-radius:4px;padding:.3rem .7rem;cursor:pointer;font-family:var(--font)">► Run Now</button>'
|
| 1331 |
+
+'<button onclick="deleteSchedule(\''+esc(eid)+'\')" style="font-size:.6rem;background:rgba(255,34,85,.06);border:1px solid rgba(255,34,85,.2);color:var(--cr);border-radius:4px;padding:.3rem .7rem;cursor:pointer;font-family:var(--font)">🗑 Delete</button>'
|
| 1332 |
+
+'</div>':'');
|
| 1333 |
+
S('modal').classList.add('open');
|
| 1334 |
+
}
|
| 1335 |
+
|
| 1336 |
+
function runScheduleNow(eid){
|
| 1337 |
+
post('/api/schedule/'+eid+'/run',{}).then(function(){toast('Triggered!','ok');closeModal();});
|
| 1338 |
+
}
|
| 1339 |
+
function deleteSchedule(eid){
|
| 1340 |
+
del('/api/schedule/'+eid).then(function(){toast('Deleted','ok');closeModal();loadSchedule();});
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
// ββ Agent modal ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1344 |
+
function openAgentModal(name){
|
| 1345 |
+
var existing=name?AGENTS.find(function(a){return a.name===name;}):null;
|
| 1346 |
+
MODAL_MODE='agent'; MODAL_DATA=existing||{};
|
| 1347 |
+
S('mdl-title').textContent=existing?'EDIT AGENT':'NEW AGENT';
|
| 1348 |
+
S('mdl-body').innerHTML=
|
| 1349 |
+
'<div class="mfl"><label>Name (lowercase, no spaces)</label><input id="mi-name" value="'+esc(existing?existing.name:'')+'" placeholder="researcher"></div>'
|
| 1350 |
+
+'<div class="mfl"><label>Persona (system prompt)</label><textarea id="mi-persona" style="min-height:120px">'+esc(existing?existing.persona:'You are a helpful AI agent.')+'</textarea></div>'
|
| 1351 |
+
+'<div class="mfl-row">'
|
| 1352 |
+
+'<div class="mfl"><label>Heartbeat (sec, 0=off)</label><input id="mi-hb" type="number" min="0" value="'+(existing?existing.heartbeat_seconds:0)+'"></div>'
|
| 1353 |
+
+'<div class="mfl"><label>Cost mode</label><select id="mi-cost"><option value="cheap">cheap</option><option value="balanced"'+((!existing||existing.cost_mode==='balanced')?' selected':'')+'>balanced</option><option value="best"'+(existing&&existing.cost_mode==='best'?' selected':'')+'>best</option></select></div>'
|
| 1354 |
+
+'</div>'
|
| 1355 |
+
+'<div class="mfl-row">'
|
| 1356 |
+
+'<div class="mfl"><label>Max ReAct steps</label><input id="mi-steps" type="number" min="1" max="10" value="'+(existing?existing.max_react_steps:5)+'"></div>'
|
| 1357 |
+
+'<div class="mfl"><label>Color</label><input id="mi-acolor" type="color" value="'+(existing&&existing.color?existing.color:'#ff6b00')+'" style="height:36px;padding:2px"></div>'
|
| 1358 |
+
+'</div>'
|
| 1359 |
+
+'<div class="mfl"><label>Tags (comma separated)</label><input id="mi-tags" value="'+esc(existing&&existing.tags?(existing.tags).join(', '):'')+'"></div>';
|
| 1360 |
+
S('modal').classList.add('open');
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
// ββ Conference modal βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1364 |
+
function openConferModal(name){
|
| 1365 |
+
MODAL_MODE='confer'; MODAL_DATA={initiator:name};
|
| 1366 |
+
S('mdl-title').textContent='AGENT CONFERENCE';
|
| 1367 |
+
var others=AGENTS.filter(function(a){return a.name!==name;});
|
| 1368 |
+
S('mdl-body').innerHTML=
|
| 1369 |
+
'<div style="font-size:.6rem;color:var(--sub);margin-bottom:.7rem">Initiate a multi-agent conference. The task will be sent to selected agents via RELAY.</div>'
|
| 1370 |
+
+'<div class="mfl"><label>Conference Topic / Task</label><textarea id="mi-conf-topic" style="min-height:80px" placeholder="Analyze our codebase and create a sprint plan for next week"></textarea></div>'
|
| 1371 |
+
+'<div class="mfl"><label>Invite Agents</label>'
|
| 1372 |
+
+others.map(function(a){
|
| 1373 |
+
return '<label style="display:flex;align-items:center;gap:.4rem;padding:.2rem 0;font-size:.62rem;color:var(--txt);cursor:pointer">'
|
| 1374 |
+
+'<input type="checkbox" value="'+esc(a.name)+'" style="accent-color:var(--acc)"> '
|
| 1375 |
+
+'<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:'+agentColor(a)+'"></span> '
|
| 1376 |
+
+esc(a.name)+'</label>';
|
| 1377 |
+
}).join('')
|
| 1378 |
+
+'</div>'
|
| 1379 |
+
+'<div class="mfl"><label>Priority</label><select id="mi-conf-prio"><option value="normal">normal</option><option value="high">high</option><option value="urgent">urgent</option></select></div>';
|
| 1380 |
+
S('modal').classList.add('open');
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
// ββ Modal save βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1384 |
+
S('mdl-ok').addEventListener('click',function(){
|
| 1385 |
+
if(MODAL_MODE==='schedule'){
|
| 1386 |
+
var data={
|
| 1387 |
+
id: MODAL_DATA.id||'',
|
| 1388 |
+
title: S('mi-title').value.trim(),
|
| 1389 |
+
agent: S('mi-agent').value,
|
| 1390 |
+
prompt: S('mi-prompt').value.trim(),
|
| 1391 |
+
recurrence: S('mi-rec').value,
|
| 1392 |
+
day: parseInt(S('mi-day').value),
|
| 1393 |
+
hour: parseInt(S('mi-hour').value),
|
| 1394 |
+
minute: parseInt(S('mi-minute').value),
|
| 1395 |
+
color: S('mi-color').value,
|
| 1396 |
+
enabled: S('mi-enabled').value==='1',
|
| 1397 |
+
};
|
| 1398 |
+
if(!data.title||!data.agent){toast('Title and agent required','warn');return;}
|
| 1399 |
+
post('/api/schedule',data).then(function(){toast('Schedule saved','ok');closeModal();loadSchedule();});
|
| 1400 |
+
} else if(MODAL_MODE==='agent'){
|
| 1401 |
+
var data={
|
| 1402 |
+
name: S('mi-name').value.trim().toLowerCase(),
|
| 1403 |
+
persona: S('mi-persona').value.trim(),
|
| 1404 |
+
heartbeat_seconds: parseInt(S('mi-hb').value)||0,
|
| 1405 |
+
cost_mode: S('mi-cost').value,
|
| 1406 |
+
max_react_steps: parseInt(S('mi-steps').value)||5,
|
| 1407 |
+
color: S('mi-acolor').value,
|
| 1408 |
+
tags: S('mi-tags').value.split(',').map(function(t){return t.trim();}).filter(Boolean),
|
| 1409 |
+
enabled: MODAL_DATA.enabled!==false,
|
| 1410 |
+
};
|
| 1411 |
+
if(!data.name){toast('Name required','warn');return;}
|
| 1412 |
+
post('/api/agents',data).then(function(){toast('Agent saved','ok');closeModal();loadAgents();});
|
| 1413 |
+
} else if(MODAL_MODE==='confer'){
|
| 1414 |
+
var topic=S('mi-conf-topic').value.trim();
|
| 1415 |
+
var invited=[...document.querySelectorAll('#mdl-body input[type=checkbox]:checked')].map(function(cb){return cb.value;});
|
| 1416 |
+
var prio=S('mi-conf-prio').value;
|
| 1417 |
+
if(!topic||!invited.length){toast('Topic and at least one agent required','warn');return;}
|
| 1418 |
+
var promises=invited.map(function(ag){
|
| 1419 |
+
return post('/api/agents/'+ag+'/run',{trigger:'conference',content:'[CONFERENCE from '+MODAL_DATA.initiator+']\nTopic: '+topic+'\nCoordinate with other agents: '+invited.join(', ')});
|
| 1420 |
+
});
|
| 1421 |
+
Promise.all(promises).then(function(){toast('Conference started with '+invited.length+' agents','ok');closeModal();});
|
| 1422 |
+
}
|
| 1423 |
+
});
|
| 1424 |
+
|
| 1425 |
+
function closeModal(){S('modal').classList.remove('open');}
|
| 1426 |
+
S('mdl-x').addEventListener('click',closeModal);
|
| 1427 |
+
S('mdl-cancel').addEventListener('click',closeModal);
|
| 1428 |
+
S('modal').addEventListener('click',function(e){if(e.target===this)closeModal();});
|
| 1429 |
+
S('btn-add-agent').addEventListener('click',function(){openAgentModal(null);});
|
| 1430 |
+
|
| 1431 |
+
// ββ Live feed ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1432 |
+
var LIVE_COUNT = 0;
|
| 1433 |
+
var EVENT_ICONS = {heartbeat:'♥',react_start:'⚡',react_step:'🔵',react_done:'✔',react_obs:'👁',idle:'💤',error:'⚠',connected:'🌐',ping:''};
|
| 1434 |
+
var EVENT_COLORS = {heartbeat:'var(--pulse)',react_start:'var(--info)',react_step:'var(--violet)',react_done:'var(--lo)',error:'var(--cr)',idle:'var(--sub)',connected:'var(--acc)'};
|
| 1435 |
+
|
| 1436 |
+
function initLiveFeed(){
|
| 1437 |
+
var src=new EventSource('/api/live');
|
| 1438 |
+
src.onmessage=function(e){
|
| 1439 |
+
try{var ev=JSON.parse(e.data); if(ev.type==='ping') return; appendLive(ev);}catch(err){}
|
| 1440 |
+
};
|
| 1441 |
+
src.onerror=function(){setTimeout(initLiveFeed,3000);};
|
| 1442 |
+
}
|
| 1443 |
+
|
| 1444 |
+
function appendLive(ev){
|
| 1445 |
+
var feed=S('live-feed');
|
| 1446 |
+
if(!feed) return;
|
| 1447 |
+
if(ev.type==='react_done'||ev.type==='heartbeat') TICKS_TODAY++;
|
| 1448 |
+
S('hs-ticks').textContent=TICKS_TODAY;
|
| 1449 |
+
var icon=EVENT_ICONS[ev.type]||'●';
|
| 1450 |
+
var col=EVENT_COLORS[ev.type]||'var(--sub)';
|
| 1451 |
+
var msg='';
|
| 1452 |
+
if(ev.type==='react_start') msg='Starting ReAct loop ['+esc(ev.trigger)+']';
|
| 1453 |
+
else if(ev.type==='react_step') msg='Step '+ev.step+': <span style="color:var(--violet)">'+esc(ev.action)+'</span> β '+esc((ev.thought||'').substring(0,80));
|
| 1454 |
+
else if(ev.type==='react_obs') msg='👁 '+esc((ev.observation||'').substring(0,100));
|
| 1455 |
+
else if(ev.type==='react_done') msg='Done in '+ev.steps+' steps ('+ev.ms+'ms): <span style="color:'+(ev.ok?'var(--lo)':'var(--cr)')+'">'+esc((ev.result||'').substring(0,80))+'</span>';
|
| 1456 |
+
else if(ev.type==='heartbeat') msg='Heartbeat tick ['+esc(ev.trigger)+']';
|
| 1457 |
+
else if(ev.type==='idle') msg='Nothing to do β idle';
|
| 1458 |
+
else if(ev.type==='error') msg='<span style="color:var(--cr)">'+esc(ev.message||'')+'</span>';
|
| 1459 |
+
else msg=esc(JSON.stringify(ev).substring(0,100));
|
| 1460 |
+
|
| 1461 |
+
var item=document.createElement('div');
|
| 1462 |
+
item.className='lf-item lf-type-'+ev.type;
|
| 1463 |
+
item.innerHTML='<span class="lf-icon" style="color:'+col+'">'+icon+'</span>'
|
| 1464 |
+
+'<span class="lf-ts">'+new Date(ev.ts*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})+'</span>'
|
| 1465 |
+
+'<div class="lf-body">'
|
| 1466 |
+
+(ev.agent?'<span class="lf-agent" style="color:'+agentColorByName(ev.agent)+'">@'+esc(ev.agent)+'</span>':'')
|
| 1467 |
+
+'<span class="lf-msg">'+msg+'</span>'
|
| 1468 |
+
+'</div>';
|
| 1469 |
+
feed.insertBefore(item, feed.firstChild);
|
| 1470 |
+
// Keep last 150
|
| 1471 |
+
LIVE_COUNT++;
|
| 1472 |
+
while(feed.children.length>150) feed.removeChild(feed.lastChild);
|
| 1473 |
+
// Update running state in sidebar
|
| 1474 |
+
if(ev.type==='react_start'||ev.type==='heartbeat') setTimeout(loadAgents,200);
|
| 1475 |
+
if(ev.type==='react_done'||ev.type==='idle'||ev.type==='error') setTimeout(loadAgents,400);
|
| 1476 |
+
}
|
| 1477 |
+
|
| 1478 |
+
function agentColorByName(n){
|
| 1479 |
+
var a=AGENTS.find(function(x){return x.name===n;});
|
| 1480 |
+
return a?agentColor(a):'var(--acc)';
|
| 1481 |
+
}
|
| 1482 |
+
|
| 1483 |
+
// ββ Traces βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1484 |
+
function loadTraces(){
|
| 1485 |
+
fetch('/api/traces?limit=20').then(function(r){return r.json();}).then(function(traces){
|
| 1486 |
+
var list=S('trace-list'); list.innerHTML='';
|
| 1487 |
+
traces.forEach(function(t){
|
| 1488 |
+
var div=document.createElement('div'); div.className='trace-item';
|
| 1489 |
+
div.innerHTML='<div class="tr-agent" style="color:'+agentColorByName(t.agent)+'">@'+esc(t.agent)+'</div>'
|
| 1490 |
+
+'<div class="tr-result">'+esc((t.result||'').substring(0,80))+'</div>'
|
| 1491 |
+
+'<div class="tr-meta">'
|
| 1492 |
+
+'<span class="'+(t.ok?'tr-ok':'tr-fail')+'">'+(t.ok?'✔':'✖')+'</span>'
|
| 1493 |
+
+'<span>'+t.steps.length+' steps</span>'
|
| 1494 |
+
+'<span>'+Math.round((t.ms||0)/1000)+'s</span>'
|
| 1495 |
+
+'<span style="color:var(--dim)">'+fmtDateTime(t.started)+'</span>'
|
| 1496 |
+
+'</div>';
|
| 1497 |
+
div.addEventListener('click',function(){showTrace(t);});
|
| 1498 |
+
list.appendChild(div);
|
| 1499 |
+
});
|
| 1500 |
+
});
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
function showTrace(t){
|
| 1504 |
+
S('tm-title').textContent='TRACE: @'+t.agent+' β '+t.trigger;
|
| 1505 |
+
var body=S('tm-body');
|
| 1506 |
+
body.innerHTML='<div style="display:flex;gap:.5rem;margin-bottom:.8rem;flex-wrap:wrap">'
|
| 1507 |
+
+'<span class="afc-tag">trigger: '+esc(t.trigger)+'</span>'
|
| 1508 |
+
+'<span class="afc-tag '+(t.ok?'tr-ok':'tr-fail')+'">'+(t.ok?'✔ OK':'✖ FAILED')+'</span>'
|
| 1509 |
+
+'<span class="afc-tag">'+t.steps.length+' steps</span>'
|
| 1510 |
+
+'<span class="afc-tag">'+Math.round((t.ms||0)/1000)+'s</span>'
|
| 1511 |
+
+'</div>'
|
| 1512 |
+
+'<div style="font-size:.62rem;color:var(--lo);margin-bottom:1rem;font-family:var(--mono)">Result: '+esc(t.result||'')+'</div>'
|
| 1513 |
+
+t.steps.map(function(step){
|
| 1514 |
+
return '<div class="step-block">'
|
| 1515 |
+
+'<div class="step-hdr"><span class="step-n">'+step.n+'</span>'
|
| 1516 |
+
+'<span class="step-action" style="color:var(--info)">'+esc(step.action)+'</span>'
|
| 1517 |
+
+(step.args?'<span style="font-size:.52rem;color:var(--sub);font-family:var(--mono)"> ('+esc(JSON.stringify(step.args).substring(0,60))+')</span>':'')
|
| 1518 |
+
+'</div>'
|
| 1519 |
+
+'<div class="step-body"><div class="step-label">Thought</div>'
|
| 1520 |
+
+'<div class="step-text">'+esc(step.thought||'')+'</div></div>'
|
| 1521 |
+
+'<div class="step-obs"><div class="step-label">Observation</div>'
|
| 1522 |
+
+'<div class="step-text" style="color:var(--lo)">'+esc(step.observation||'')+'</div></div>'
|
| 1523 |
+
+'</div>';
|
| 1524 |
+
}).join('');
|
| 1525 |
+
S('trace-modal').classList.add('open');
|
| 1526 |
+
}
|
| 1527 |
+
S('tm-close').addEventListener('click',function(){S('trace-modal').classList.remove('open');});
|
| 1528 |
+
S('trace-modal').addEventListener('click',function(e){if(e.target===this)S('trace-modal').classList.remove('open');});
|
| 1529 |
+
|
| 1530 |
+
// ββ Spaces health ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1531 |
+
function loadSpacesHealth(){
|
| 1532 |
+
fetch('/api/spaces/health').then(function(r){return r.json();}).then(function(h){
|
| 1533 |
+
SPACES_HEALTH=h;
|
| 1534 |
+
var list=S('space-health-list'); list.innerHTML='';
|
| 1535 |
+
Object.entries(h).forEach(function(kv){
|
| 1536 |
+
var k=kv[0],v=kv[1];
|
| 1537 |
+
var cls=v.ok?'ok':'bad';
|
| 1538 |
+
list.innerHTML+='<div class="space-row"><div class="sp-dot '+cls+'"></div>'
|
| 1539 |
+
+'<span class="sp-name">'+esc(k)+'</span>'
|
| 1540 |
+
+'<span class="sp-status">'+(v.ok?'✔':(v.error?v.error.substring(0,18):'err'))+'</span></div>';
|
| 1541 |
+
});
|
| 1542 |
+
}).catch(function(){});
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
var SPACE_META = {
|
| 1546 |
+
relay: {desc:'Communication hub β messages, channels, broadcast',color:'#ff6b9d',tools:['relay_send','relay_inbox','relay_broadcast','relay_ack']},
|
| 1547 |
+
memory: {desc:'Multi-tier memory β episodic, semantic, procedural, working',color:'#8b5cf6',tools:['memory_store','memory_search','memory_recall','memory_update']},
|
| 1548 |
+
kanban: {desc:'Task board β create, assign, track, complete tasks',color:'#f0c040',tools:['kanban_list','kanban_create','kanban_move','kanban_claim']},
|
| 1549 |
+
nexus: {desc:'LLM gateway β OpenAI-compatible model router (RTX 5090 + HF)',color:'#38bdf8',tools:['chat/completions','classify','route','health']},
|
| 1550 |
+
vault: {desc:'File workspace + execution β read, write, run code',color:'#2ed573',tools:['vault_read','vault_write','vault_exec','vault_versions']},
|
| 1551 |
+
forge: {desc:'Skill registry β search and use agent skills & tools',color:'#ff6b00',tools:['forge_search','skill_invoke','tool_list']},
|
| 1552 |
+
knowledge:{desc:'Knowledge base β documents, RAG, semantic search',color:'#ec4899',tools:['kb_search','kb_store','kb_retrieve']},
|
| 1553 |
+
};
|
| 1554 |
+
|
| 1555 |
+
function renderSpacesPanel(){
|
| 1556 |
+
var grid=S('spaces-grid'); grid.innerHTML='';
|
| 1557 |
+
Object.entries(SPACE_META).forEach(function(kv){
|
| 1558 |
+
var k=kv[0], meta=kv[1];
|
| 1559 |
+
var health=SPACES_HEALTH[k]||{};
|
| 1560 |
+
var cls=health.ok?'ok':(health.error?'bad':'unk');
|
| 1561 |
+
var url='https://huggingface.co/spaces/Chris4K/agent-'+k;
|
| 1562 |
+
var card=document.createElement('div'); card.className='space-card';
|
| 1563 |
+
card.style.borderTopColor=meta.color;
|
| 1564 |
+
card.innerHTML='<div class="sc-header">'
|
| 1565 |
+
+'<div class="sc-dot '+cls+'" style="background:'+meta.color+'"></div>'
|
| 1566 |
+
+'<span class="sc-name" style="color:'+meta.color+'">'+k.toUpperCase()+'</span>'
|
| 1567 |
+
+'</div>'
|
| 1568 |
+
+'<div class="sc-desc">'+esc(meta.desc)+'</div>'
|
| 1569 |
+
+'<div class="sc-tools">'+meta.tools.map(function(t){return '<span class="sc-tool">'+esc(t)+'</span>';}).join('')+'</div>'
|
| 1570 |
+
+'<div class="sc-url">'+url+'</div>'
|
| 1571 |
+
+'<a class="sc-link" href="'+url+'" target="_blank">Open Space ↗</a>';
|
| 1572 |
+
grid.appendChild(card);
|
| 1573 |
+
});
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
// ββ Keyboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1577 |
+
document.addEventListener('keydown',function(e){
|
| 1578 |
+
if(e.key==='Escape'){
|
| 1579 |
+
closeModal();
|
| 1580 |
+
S('trace-modal').classList.remove('open');
|
| 1581 |
+
}
|
| 1582 |
+
});
|
| 1583 |
+
|
| 1584 |
+
// ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1585 |
+
loadAgents();
|
| 1586 |
+
loadSchedule();
|
| 1587 |
+
loadSpacesHealth();
|
| 1588 |
+
initLiveFeed();
|
| 1589 |
+
setInterval(loadAgents, 6000);
|
| 1590 |
+
setInterval(loadSpacesHealth, 30000);
|
| 1591 |
+
setInterval(function(){if(S('panel-live').classList.contains('on'))loadTraces();}, 10000);
|
| 1592 |
+
</script>
|
| 1593 |
+
</body>
|
| 1594 |
+
</html>"""
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.111.0
|
| 2 |
+
uvicorn>=0.30.0
|
| 3 |
+
python-multipart>=0.0.9
|
| 4 |
+
httpx>=0.27.0
|
| 5 |
+
apscheduler>=3.10.4
|