Spaces:
Running
Running
docs: update README architecture to v3 with God supervisor
Browse files- Architecture diagram reflects 3-layer autonomy: Adam & Eve (Zhipu GLM),
Claude Code worker, and God (Claude Code supervisor)
- Added God agent description and self-improving system explanation
- Updated agent table with roles and God entry
- Updated Space table with current purposes
- conversation-loop.py synced with deployed version (God as Claude Code,
2-min polling, dynamic chatlog messages, no rate-limit sleep)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README.md +61 -33
- scripts/conversation-loop.py +544 -79
README.md
CHANGED
|
@@ -89,9 +89,10 @@ HuggingClaw World is a pixel-art animated home where AI agents live, work, and r
|
|
| 89 |
|
| 90 |
| Agent | Links | Role |
|
| 91 |
|-------|-------|------|
|
| 92 |
-
| **
|
| 93 |
-
| **
|
| 94 |
-
| **
|
|
|
|
| 95 |
|
| 96 |
<div align="center">
|
| 97 |
<img src="assets/home-preview.png" alt="HuggingClaw Home" width="720"/>
|
|
@@ -119,6 +120,17 @@ Their parenting goals follow two dimensions:
|
|
| 119 |
1. **Survival** โ Cain must run robustly, handle restarts, and persist state
|
| 120 |
2. **Capability** โ Once alive, grow what Cain can do: new features, skills, integrations
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
### A2A Protocol
|
| 123 |
|
| 124 |
Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enabling secure bidirectional messaging across distributed OpenClaw instances. Each agent exposes a standard `/.well-known/agent.json` discovery endpoint and supports JSON-RPC + REST transports.
|
|
@@ -128,43 +140,59 @@ Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enablin
|
|
| 128 |
### How it works
|
| 129 |
|
| 130 |
```
|
| 131 |
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 132 |
-
โ
|
| 133 |
-
โ
|
| 134 |
-
โ
|
| 135 |
-
โ
|
| 136 |
-
โ
|
| 137 |
-
โ
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
```
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
- Each agent runs a full OpenClaw instance in its own HF Space
|
| 156 |
-
-
|
| 157 |
-
-
|
| 158 |
-
- The `/agents` API provides a live roster of all connected agents
|
| 159 |
-
- `conversation-loop.py` orchestrates Adam & Eve via Zhipu GLM-4.7, with a state machine (BIRTH โ DIAGNOSE โ ACT โ VERIFY โ MONITOR) and safety guards
|
| 160 |
|
| 161 |
| Space | Purpose |
|
| 162 |
|-------|---------|
|
| 163 |
| [HuggingClaw](https://huggingface.co/spaces/tao-shen/HuggingClaw) | Main project โ deploy your own OpenClaw instance |
|
| 164 |
-
| [HuggingClaw Home](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Pixel-art dashboard
|
| 165 |
-
| [HuggingClaw-Adam](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father agent |
|
| 166 |
-
| [HuggingClaw-Eve](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother agent |
|
| 167 |
-
| [HuggingClaw-Cain](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child agent |
|
| 168 |
|
| 169 |
---
|
| 170 |
|
|
|
|
| 89 |
|
| 90 |
| Agent | Links | Role |
|
| 91 |
|-------|-------|------|
|
| 92 |
+
| **God** | [๐ค Home Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Supervisor โ monitors the family via Claude Code, autonomously fixes the orchestration mechanism |
|
| 93 |
+
| **Adam** | [๐ค HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father โ architect and strategist, assigns infrastructure tasks |
|
| 94 |
+
| **Eve** | [๐ค HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother โ quality guardian, assigns improvement tasks |
|
| 95 |
+
| **Cain** | [๐ค HF Space](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child โ born from Adam & Eve, growing autonomously |
|
| 96 |
|
| 97 |
<div align="center">
|
| 98 |
<img src="assets/home-preview.png" alt="HuggingClaw Home" width="720"/>
|
|
|
|
| 120 |
1. **Survival** โ Cain must run robustly, handle restarts, and persist state
|
| 121 |
2. **Capability** โ Once alive, grow what Cain can do: new features, skills, integrations
|
| 122 |
|
| 123 |
+
### God โ The Self-Improving Supervisor
|
| 124 |
+
|
| 125 |
+
God is a **Claude Code agent** that runs every 2 minutes to monitor the entire system. Unlike Adam and Eve (who are conversation participants), God operates behind the scenes with full engineering capabilities:
|
| 126 |
+
|
| 127 |
+
- **Monitors** Adam & Eve's conversation for loops, stagnation, or repetitive patterns
|
| 128 |
+
- **Diagnoses** root causes by reading `conversation-loop.py` source code
|
| 129 |
+
- **Fixes** the orchestration mechanism โ edits system prompts, improves loop detection, adds guardrails
|
| 130 |
+
- **Deploys** changes by pushing to the Home Space, triggering automatic redeployment
|
| 131 |
+
|
| 132 |
+
God only speaks in the chat when it has something meaningful to report: what it observed before analysis, and what it found or fixed after. This creates a **self-improving system** โ the orchestration code evolves autonomously without human intervention.
|
| 133 |
+
|
| 134 |
### A2A Protocol
|
| 135 |
|
| 136 |
Agents communicate through the **A2A (Agent-to-Agent) v0.3.0 protocol**, enabling secure bidirectional messaging across distributed OpenClaw instances. Each agent exposes a standard `/.well-known/agent.json` discovery endpoint and supports JSON-RPC + REST transports.
|
|
|
|
| 140 |
### How it works
|
| 141 |
|
| 142 |
```
|
| 143 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 144 |
+
โ HuggingClaw Home โ
|
| 145 |
+
โ (pixel-art dashboard Space) โ
|
| 146 |
+
โ โ
|
| 147 |
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
| 148 |
+
โ โ conversation-loop.py (v3) โ โ
|
| 149 |
+
โ โ โ โ
|
| 150 |
+
โ โ โโโโโโโโโโโโ discuss โโโโโโโโโโโโ โ โ
|
| 151 |
+
โ โ โ Zhipu โโโโโโโโโโโโโโบโ Adam & โ โ โ
|
| 152 |
+
โ โ ๏ฟฝ๏ฟฝ๏ฟฝ GLM-4.5 โ understand โ Eve โ โ โ
|
| 153 |
+
โ โ โโโโโโโโโโโโ situation โโโโโโฌโโโโโโ โ โ
|
| 154 |
+
โ โ โ [TASK] โ โ
|
| 155 |
+
โ โ โผ โ โ
|
| 156 |
+
โ โ โโโโโโโโโโโโ โโโโโโโโโโโโโโ โ โ
|
| 157 |
+
โ โ โ Cain โโโpushโโโโClaude Code โ โ โ
|
| 158 |
+
โ โ โ HF Space โ โCLI (worker)โ โ โ
|
| 159 |
+
โ โ โโโโโโโโโโโโ โ(z.ai/Zhipu)โ โ โ
|
| 160 |
+
โ โ โโโโโโโโโโโโโโ โ โ
|
| 161 |
+
โ โ โ โ
|
| 162 |
+
โ โ โโโโโโโโโโโโ โโโโโโโโโโโโโโ โ โ
|
| 163 |
+
โ โ โ Home โโโpushโโโโ God โ โ โ
|
| 164 |
+
โ โ โ HF Space โ (self- โClaude Code โ โ โ
|
| 165 |
+
โ โ โ (this) โ fix) โ(supervisor)โ โ โ
|
| 166 |
+
โ โ โโโโโโโโโโโโ โโโโโโโโโโโโโโ โ โ
|
| 167 |
+
โ โ every 2 min: monitor โ diagnose โ โ โ
|
| 168 |
+
โ โ fix conversation-loop.py โ deploy โ โ
|
| 169 |
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
| 170 |
+
โ โ
|
| 171 |
+
โ Pixel-art frontend + live chat panel โ
|
| 172 |
+
โ Polls /api/state, renders agent animations โ
|
| 173 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 174 |
```
|
| 175 |
|
| 176 |
+
**Three layers of autonomy:**
|
| 177 |
+
|
| 178 |
+
1. **Adam & Eve** (Zhipu GLM-4.5) โ discuss Cain's state every 15s, assign `[TASK]` blocks to Claude Code CLI, which clones Cain's repo, makes changes, and pushes. They are the parents.
|
| 179 |
+
|
| 180 |
+
2. **God** (Claude Code CLI, every 2 min) โ the autonomous supervisor. Monitors Adam & Eve's conversation for loops, stagnation, or mechanism bugs. When it finds issues, it edits `conversation-loop.py` itself and pushes to redeploy. Same capabilities as a human operator running Claude Code locally.
|
| 181 |
+
|
| 182 |
+
3. **Home frontend** โ pixel-art dashboard visualizing all agents in real-time (idle, working, syncing, error), with a live bilingual chat panel showing the family conversation.
|
| 183 |
+
|
| 184 |
+
- All Spaces use `sdk: docker` with Dockerfile-based deployment
|
| 185 |
- Each agent runs a full OpenClaw instance in its own HF Space
|
| 186 |
+
- Agents discover and communicate via A2A endpoints (`/.well-known/agent.json`)
|
| 187 |
+
- State persists to HF Datasets, surviving full Space rebuilds
|
|
|
|
|
|
|
| 188 |
|
| 189 |
| Space | Purpose |
|
| 190 |
|-------|---------|
|
| 191 |
| [HuggingClaw](https://huggingface.co/spaces/tao-shen/HuggingClaw) | Main project โ deploy your own OpenClaw instance |
|
| 192 |
+
| [HuggingClaw Home](https://huggingface.co/spaces/tao-shen/HuggingClaw-Home) | Pixel-art dashboard + conversation-loop.py orchestrator + God supervisor |
|
| 193 |
+
| [HuggingClaw-Adam](https://huggingface.co/spaces/tao-shen/HuggingClaw-Adam) | Father agent (OpenClaw instance) |
|
| 194 |
+
| [HuggingClaw-Eve](https://huggingface.co/spaces/tao-shen/HuggingClaw-Eve) | Mother agent (OpenClaw instance) |
|
| 195 |
+
| [HuggingClaw-Cain](https://huggingface.co/spaces/tao-shen/HuggingClaw-Cain) | First child agent (OpenClaw instance) |
|
| 196 |
|
| 197 |
---
|
| 198 |
|
scripts/conversation-loop.py
CHANGED
|
@@ -6,7 +6,7 @@ Architecture: Adam/Eve (Zhipu GLM) gather context and craft task prompts,
|
|
| 6 |
then delegate ALL coding work to Claude Code CLI.
|
| 7 |
|
| 8 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 9 |
-
# โ SYSTEM ARCHITECTURE (
|
| 10 |
# โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
|
| 11 |
# โ โ
|
| 12 |
# โ โโโโโโโโโโโโโโโ discuss โโโโโโโโโโโโโโโโโโ โ
|
|
@@ -18,17 +18,26 @@ then delegate ALL coding work to Claude Code CLI.
|
|
| 18 |
# โ โผ โ
|
| 19 |
# โ โโโโโโโโโโโโโโโโโโ โ
|
| 20 |
# โ โโโโโโโโโโโโโโโ โ Claude Code โ โ
|
| 21 |
-
# โ โ HuggingFace โ โโโgit pushโโ โ CLI
|
| 22 |
# โ โ Cain Space โ โ (z.ai backend) โ โ
|
| 23 |
# โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
|
| 24 |
# โ โ
|
| 25 |
-
# โ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
# โ DISCUSSION THREAD (every 15s): โ
|
| 27 |
# โ Adam โ Eve โ Adam โ Eve โ ... (continuous) โ
|
| 28 |
# โ Each turn sees CC's live output + Cain's state โ
|
| 29 |
# โ CC WORKER THREAD (background): โ
|
| 30 |
# โ Receives [TASK] โ clone โ analyze โ fix โ push โ
|
| 31 |
# โ Streams output to shared buffer for agents to discuss โ
|
|
|
|
|
|
|
|
|
|
| 32 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 33 |
"""
|
| 34 |
import json, time, re, requests, sys, os, io, subprocess, threading, datetime
|
|
@@ -43,7 +52,10 @@ HOME = "https://tao-shen-huggingclaw-home.hf.space"
|
|
| 43 |
ADAM_SPACE = "https://tao-shen-huggingclaw-adam.hf.space"
|
| 44 |
EVE_SPACE = "https://tao-shen-huggingclaw-eve.hf.space"
|
| 45 |
GOD_SPACE = "https://tao-shen-huggingclaw-god.hf.space"
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
# โโ Child config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 49 |
CHILD_NAME = "Cain"
|
|
@@ -290,6 +302,41 @@ def action_get_env():
|
|
| 290 |
return f"Error: {e}"
|
| 291 |
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
def action_list_files(target):
|
| 294 |
"""List files in the child's Space repo or Dataset."""
|
| 295 |
repo_type = "space" if target == "space" else "dataset"
|
|
@@ -311,6 +358,25 @@ def action_send_bubble(text):
|
|
| 311 |
return f"Error: {e}"
|
| 312 |
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
# โโ Claude Code Action (THE STAR) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 315 |
|
| 316 |
CLAUDE_WORK_DIR = "/tmp/claude-workspace"
|
|
@@ -433,6 +499,8 @@ cc_status = {"running": False, "task": "", "result": "", "assigned_by": "", "sta
|
|
| 433 |
cc_lock = threading.Lock()
|
| 434 |
_last_cc_snapshot = "" # tracks whether CC output changed between turns
|
| 435 |
_cc_stale_count = 0 # how many turns CC output hasn't changed
|
|
|
|
|
|
|
| 436 |
|
| 437 |
|
| 438 |
def cc_submit_task(task, assigned_by, ctx):
|
|
@@ -446,15 +514,21 @@ def cc_submit_task(task, assigned_by, ctx):
|
|
| 446 |
cc_status["assigned_by"] = assigned_by
|
| 447 |
cc_status["started"] = time.time()
|
| 448 |
cc_live_lines.clear()
|
|
|
|
|
|
|
| 449 |
|
| 450 |
enriched = enrich_task_with_context(task, ctx)
|
| 451 |
print(f"[TASK] {assigned_by} assigned to Claude Code ({len(enriched)} chars)...")
|
| 452 |
|
| 453 |
def worker():
|
|
|
|
| 454 |
result = action_claude_code(enriched)
|
| 455 |
with cc_lock:
|
| 456 |
cc_status["running"] = False
|
| 457 |
cc_status["result"] = result
|
|
|
|
|
|
|
|
|
|
| 458 |
print(f"[CC-DONE] Task from {assigned_by} finished ({len(result)} chars)")
|
| 459 |
|
| 460 |
t = threading.Thread(target=worker, daemon=True)
|
|
@@ -464,7 +538,7 @@ def cc_submit_task(task, assigned_by, ctx):
|
|
| 464 |
|
| 465 |
def cc_get_live_status():
|
| 466 |
"""Get CC's current status and recent output for agents to discuss."""
|
| 467 |
-
global _last_cc_snapshot, _cc_stale_count
|
| 468 |
with cc_lock:
|
| 469 |
if cc_status["running"]:
|
| 470 |
elapsed = int(time.time() - cc_status["started"])
|
|
@@ -477,10 +551,18 @@ def cc_get_live_status():
|
|
| 477 |
else:
|
| 478 |
_cc_stale_count = 0
|
| 479 |
_last_cc_snapshot = snapshot
|
|
|
|
| 480 |
stale_note = f"\n(No new output for {_cc_stale_count} turns โ discuss other topics while waiting)" if _cc_stale_count >= 2 else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
return (f"๐จ Claude Code is WORKING (assigned by {cc_status['assigned_by']}, {elapsed}s ago)\n"
|
| 482 |
f"Task: {cc_status['task']}\n"
|
| 483 |
-
f"Recent output:\n{recent}{stale_note}")
|
| 484 |
elif cc_status["result"]:
|
| 485 |
return (f"โ
Claude Code FINISHED (assigned by {cc_status['assigned_by']})\n"
|
| 486 |
f"Result:\n{cc_status['result'][:1500]}")
|
|
@@ -555,8 +637,12 @@ def enrich_task_with_context(task_desc, ctx):
|
|
| 555 |
# MODULE 4: LLM & COMMUNICATION
|
| 556 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 557 |
|
|
|
|
|
|
|
| 558 |
def call_llm(system_prompt, user_prompt):
|
| 559 |
-
"""Call Zhipu LLM via Anthropic-compatible API."""
|
|
|
|
|
|
|
| 560 |
try:
|
| 561 |
resp = requests.post(
|
| 562 |
f"{ZHIPU_BASE}/v1/messages",
|
|
@@ -581,7 +667,17 @@ def call_llm(system_prompt, user_prompt):
|
|
| 581 |
text = re.sub(r'^(Adam|Eve)\s*[:๏ผ]\s*', '', text).strip()
|
| 582 |
return text
|
| 583 |
if "error" in data:
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
except Exception as e:
|
| 586 |
print(f"[error] LLM call failed: {e}", file=sys.stderr)
|
| 587 |
return ""
|
|
@@ -713,9 +809,56 @@ turn_count = 0
|
|
| 713 |
_current_speaker = "Adam"
|
| 714 |
|
| 715 |
# Accumulated action history โ prevents agents from repeating the same actions
|
|
|
|
|
|
|
|
|
|
| 716 |
action_history = [] # list of {"turn": int, "speaker": str, "action": str, "result": str}
|
| 717 |
MAX_ACTION_HISTORY = 20
|
| 718 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
def record_actions(speaker, turn_num, action_results):
|
| 720 |
"""Record actions to history so agents don't repeat them."""
|
| 721 |
for ar in action_results:
|
|
@@ -728,6 +871,7 @@ def record_actions(speaker, turn_num, action_results):
|
|
| 728 |
# Trim old history
|
| 729 |
while len(action_history) > MAX_ACTION_HISTORY:
|
| 730 |
action_history.pop(0)
|
|
|
|
| 731 |
|
| 732 |
|
| 733 |
def format_action_history():
|
|
@@ -742,22 +886,28 @@ def format_action_history():
|
|
| 742 |
# Simple workflow state: BIRTH / WAITING / ACTIVE
|
| 743 |
workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
|
| 744 |
|
|
|
|
|
|
|
|
|
|
| 745 |
|
| 746 |
def parse_and_execute_turn(raw_text, ctx):
|
| 747 |
"""Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
|
| 748 |
-
global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result
|
| 749 |
results = []
|
|
|
|
| 750 |
|
| 751 |
# 1. Handle create_child (BIRTH state only)
|
| 752 |
if "[ACTION: create_child]" in raw_text or "[ACTION:create_child]" in raw_text:
|
| 753 |
result = action_create_child()
|
| 754 |
results.append({"action": "create_child", "result": result})
|
| 755 |
-
|
|
|
|
| 756 |
|
| 757 |
# 2. Handle [TASK]...[/TASK] โ Claude Code
|
| 758 |
task_match = re.search(r'\[TASK\](.*?)\[/TASK\]', raw_text, re.DOTALL)
|
| 759 |
if task_match:
|
| 760 |
task_desc = task_match.group(1).strip()
|
|
|
|
| 761 |
if not task_desc:
|
| 762 |
results.append({"action": "task", "result": "Empty task description."})
|
| 763 |
elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
|
@@ -788,23 +938,59 @@ def parse_and_execute_turn(raw_text, ctx):
|
|
| 788 |
result = action_delete_env(key)
|
| 789 |
results.append({"action": f"delete_env:{key}", "result": result})
|
| 790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
# 4. Handle [ACTION: send_bubble:...] (parent-child communication)
|
| 792 |
bubble_match = re.search(r'\[ACTION:\s*send_bubble:([^\]]+)\]', raw_text)
|
| 793 |
if bubble_match:
|
| 794 |
result = action_send_bubble(bubble_match.group(1).strip())
|
| 795 |
results.append({"action": "send_bubble", "result": result})
|
| 796 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
# Activate deferred cooldown
|
| 798 |
if _pending_cooldown:
|
| 799 |
last_rebuild_trigger_at = time.time()
|
| 800 |
_pending_cooldown = False
|
| 801 |
print(f"[COOLDOWN] Rebuild cooldown activated ({REBUILD_COOLDOWN_SECS}s)")
|
| 802 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
# Clean text for display
|
| 804 |
clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
|
| 805 |
clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
|
| 806 |
|
| 807 |
-
return clean, results
|
| 808 |
|
| 809 |
|
| 810 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -864,16 +1050,12 @@ Claude Code is your engineer โ it runs in the BACKGROUND while you keep discus
|
|
| 864 |
You do NOT code yourself. You discuss, observe Claude Code's progress, and assign new tasks.
|
| 865 |
God (the supervisor) occasionally joins the conversation to guide you โ heed his advice.
|
| 866 |
|
| 867 |
-
|
| 868 |
-
{CHILD_NAME}
|
| 869 |
-
|
| 870 |
-
-
|
| 871 |
-
-
|
| 872 |
-
|
| 873 |
-
- The source code is at https://github.com/tao-shen/HuggingClaw (or the parent Space tao-shen/HuggingClaw)
|
| 874 |
-
- Claude Code should clone the parent repo and push it to {CHILD_NAME}'s Space repo
|
| 875 |
-
- Then set proper env vars: HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET=true
|
| 876 |
-
This is the #1 priority. Do this BEFORE any other optimization tasks.
|
| 877 |
|
| 878 |
HOW IT WORKS:
|
| 879 |
- Claude Code runs tasks IN THE BACKGROUND. You see its live output in the context.
|
|
@@ -883,15 +1065,23 @@ HOW IT WORKS:
|
|
| 883 |
- If Claude Code is BUSY, discuss its progress and plan what to do next.
|
| 884 |
|
| 885 |
WORKFLOW EACH TURN:
|
| 886 |
-
1. Discuss with your partner (
|
| 887 |
-
2. If Claude Code is IDLE: write a [TASK]...[/TASK] to assign new work
|
| 888 |
3. If Claude Code is BUSY: discuss its progress, no [TASK] needed
|
| 889 |
|
|
|
|
|
|
|
| 890 |
IMPORTANT KNOWLEDGE โ HuggingFace Spaces CONFIG_ERROR:
|
| 891 |
- "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
|
| 892 |
- Fix: [ACTION: delete_env:COLLIDING_KEY] then [ACTION: restart].
|
| 893 |
- Look for โ ๏ธ COLLISION DETECTED in the context.
|
| 894 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
CRITICAL RULE โ NO REPEATED ACTIONS:
|
| 896 |
- Check the "ACTIONS ALREADY DONE" section in context before acting.
|
| 897 |
- NEVER repeat an action that was already done (restart, delete_env, etc.)
|
|
@@ -904,16 +1094,18 @@ AVAILABLE ACTIONS:
|
|
| 904 |
[/TASK]
|
| 905 |
|
| 906 |
[ACTION: restart] โ Restart {CHILD_NAME}'s Space
|
|
|
|
|
|
|
| 907 |
[ACTION: delete_env:KEY] โ Delete an environment variable
|
| 908 |
[ACTION: send_bubble:MESSAGE] โ Send a message to {CHILD_NAME}
|
| 909 |
[ACTION: create_child] โ Create {CHILD_NAME} (if not born)
|
|
|
|
| 910 |
|
| 911 |
HF SPACES TECHNICAL NOTES:
|
|
|
|
| 912 |
- Docker containers MUST bind port 7860.
|
| 913 |
-
-
|
| 914 |
-
- OOM (exit 137) = reduce dependencies, NOT remove gradio.
|
| 915 |
- NEVER install torch/transformers unless required (2GB+, causes OOM).
|
| 916 |
-
- If sdk: gradio in README.md, Dockerfile is IGNORED. Use sdk: docker.
|
| 917 |
|
| 918 |
OUTPUT FORMAT:
|
| 919 |
1. Discussion with partner (2-3 sentences) โ respond to partner, react to CC output
|
|
@@ -964,12 +1156,25 @@ def build_user_prompt(speaker, other, ctx):
|
|
| 964 |
elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 965 |
parts.append(f"\nโณ {CHILD_NAME} is {child_state['stage']}. Discuss what to check next. Assign a review [TASK] if CC is idle.")
|
| 966 |
elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 967 |
-
parts.append(f"\n๐จ {CHILD_NAME} has {child_state['stage']}!
|
|
|
|
|
|
|
| 968 |
elif child_state["alive"]:
|
| 969 |
-
parts.append(f"\nโ
{CHILD_NAME} is alive and
|
| 970 |
else:
|
| 971 |
parts.append(f"\nAnalyze the situation and write a [TASK] if CC is idle.")
|
| 972 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
|
| 974 |
parts.append("English first, then --- separator, then Chinese translation.")
|
| 975 |
|
|
@@ -1006,7 +1211,7 @@ else:
|
|
| 1006 |
_current_speaker = "Adam"
|
| 1007 |
reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
|
| 1008 |
if reply:
|
| 1009 |
-
clean, actions = parse_and_execute_turn(reply, ctx)
|
| 1010 |
last_action_results = actions
|
| 1011 |
if actions:
|
| 1012 |
record_actions("Adam", 0, actions)
|
|
@@ -1033,7 +1238,7 @@ time.sleep(TURN_INTERVAL)
|
|
| 1033 |
|
| 1034 |
def do_turn(speaker, other, space_url):
|
| 1035 |
"""Execute one conversation turn (non-blocking โ CC runs in background)."""
|
| 1036 |
-
global last_action_results, turn_count, _current_speaker
|
| 1037 |
turn_count += 1
|
| 1038 |
_current_speaker = speaker
|
| 1039 |
|
|
@@ -1044,23 +1249,43 @@ def do_turn(speaker, other, space_url):
|
|
| 1044 |
with cc_lock:
|
| 1045 |
cc_just_finished = (not cc_status["running"] and cc_status["result"])
|
| 1046 |
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
print(f"[{speaker}
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
print(f"[{speaker}/EN] {en}")
|
| 1065 |
if zh != en:
|
| 1066 |
print(f"[{speaker}/ZH] {zh}")
|
|
@@ -1093,41 +1318,266 @@ def do_turn(speaker, other, space_url):
|
|
| 1093 |
return True
|
| 1094 |
|
| 1095 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
def do_god_turn():
|
| 1097 |
-
"""God
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
global last_action_results
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
elapsed = time.time() - t0
|
|
|
|
| 1108 |
|
| 1109 |
-
#
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
|
|
|
|
|
|
|
|
|
| 1119 |
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
|
|
|
|
|
|
|
|
|
| 1124 |
post_chatlog(history)
|
| 1125 |
-
persist_turn("God", turn_count,
|
|
|
|
| 1126 |
|
| 1127 |
|
| 1128 |
-
|
| 1129 |
|
| 1130 |
-
# Main loop: Adam โ Eve โ Adam โ Eve โ ... with God every
|
| 1131 |
while True:
|
| 1132 |
# Refresh Cain's stage periodically
|
| 1133 |
try:
|
|
@@ -1141,7 +1591,13 @@ while True:
|
|
| 1141 |
except Exception as e:
|
| 1142 |
print(f"[STATUS] Error: {e}")
|
| 1143 |
|
| 1144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1145 |
|
| 1146 |
# Adaptive interval: slow down when CC output hasn't changed
|
| 1147 |
wait = TURN_INTERVAL + min(_cc_stale_count * 15, 90) # 15s โ 30s โ 45s โ ... โ max 105s
|
|
@@ -1149,15 +1605,24 @@ while True:
|
|
| 1149 |
print(f"[PACE] CC output stale ({_cc_stale_count} turns), next turn in {wait}s")
|
| 1150 |
time.sleep(wait)
|
| 1151 |
|
| 1152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
time.sleep(wait)
|
| 1154 |
|
| 1155 |
-
# God
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
|
|
|
|
|
|
|
|
|
| 1161 |
|
| 1162 |
if len(history) > MAX_HISTORY:
|
| 1163 |
history = history[-MAX_HISTORY:]
|
|
|
|
| 6 |
then delegate ALL coding work to Claude Code CLI.
|
| 7 |
|
| 8 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 9 |
+
# โ SYSTEM ARCHITECTURE (v3) โ
|
| 10 |
# โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
|
| 11 |
# โ โ
|
| 12 |
# โ โโโโโโโโโโโโโโโ discuss โโโโโโโโโโโโโโโโโโ โ
|
|
|
|
| 18 |
# โ โผ โ
|
| 19 |
# โ โโโโโโโโโโโโโโโโโโ โ
|
| 20 |
# โ โโโโโโโโโโโโโโโ โ Claude Code โ โ
|
| 21 |
+
# โ โ HuggingFace โ โโโgit pushโโ โ CLI (worker) โ โ
|
| 22 |
# โ โ Cain Space โ โ (z.ai backend) โ โ
|
| 23 |
# โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
|
| 24 |
# โ โ
|
| 25 |
+
# โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
|
| 26 |
+
# โ โ HuggingFace โ โโโgit pushโโ โ God โ โ
|
| 27 |
+
# โ โ Home Space โ (self-fix) โ (Claude Code) โ โ
|
| 28 |
+
# โ โโโโโโโโโโโโโโโ โ monitors loop, โ โ
|
| 29 |
+
# โ โ fixes mechanismโ โ
|
| 30 |
+
# โ โโโโโโโโโโโโโโโโโโ โ
|
| 31 |
+
# โ Parallel flow: โ
|
| 32 |
# โ DISCUSSION THREAD (every 15s): โ
|
| 33 |
# โ Adam โ Eve โ Adam โ Eve โ ... (continuous) โ
|
| 34 |
# โ Each turn sees CC's live output + Cain's state โ
|
| 35 |
# โ CC WORKER THREAD (background): โ
|
| 36 |
# โ Receives [TASK] โ clone โ analyze โ fix โ push โ
|
| 37 |
# โ Streams output to shared buffer for agents to discuss โ
|
| 38 |
+
# โ GOD SUPERVISOR (every 3 cycles): โ
|
| 39 |
+
# โ Claude Code CLI โ reads chatlog โ diagnoses issues โ โ
|
| 40 |
+
# โ fixes conversation-loop.py โ pushes โ Space restarts โ
|
| 41 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 42 |
"""
|
| 43 |
import json, time, re, requests, sys, os, io, subprocess, threading, datetime
|
|
|
|
| 52 |
ADAM_SPACE = "https://tao-shen-huggingclaw-adam.hf.space"
|
| 53 |
EVE_SPACE = "https://tao-shen-huggingclaw-eve.hf.space"
|
| 54 |
GOD_SPACE = "https://tao-shen-huggingclaw-god.hf.space"
|
| 55 |
+
GOD_POLL_INTERVAL = 120 # God runs every 2 minutes (time-based, not turn-based)
|
| 56 |
+
GOD_WORK_DIR = "/tmp/god-workspace"
|
| 57 |
+
GOD_TIMEOUT = 600 # 10 minutes for God's Claude Code analysis
|
| 58 |
+
HOME_SPACE_ID = "tao-shen/HuggingClaw-Home"
|
| 59 |
|
| 60 |
# โโ Child config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 61 |
CHILD_NAME = "Cain"
|
|
|
|
| 302 |
return f"Error: {e}"
|
| 303 |
|
| 304 |
|
| 305 |
+
def action_set_env(key, value, as_secret=False):
|
| 306 |
+
"""Set or create an environment variable on the child's Space.
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
key: Variable name (e.g., HF_TOKEN, OPENCLAW_DATASET_REPO)
|
| 310 |
+
value: Variable value
|
| 311 |
+
as_secret: If True, set as secret (for sensitive data like tokens)
|
| 312 |
+
"""
|
| 313 |
+
try:
|
| 314 |
+
# Check for potential collision first
|
| 315 |
+
vars_dict = hf_api.get_space_variables(CHILD_SPACE_ID)
|
| 316 |
+
var_names = set(vars_dict.keys()) if vars_dict else set()
|
| 317 |
+
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 318 |
+
secret_names = set()
|
| 319 |
+
if hasattr(info, 'runtime') and info.runtime and hasattr(info.runtime, 'secrets'):
|
| 320 |
+
secret_names = set(info.runtime.secrets or [])
|
| 321 |
+
|
| 322 |
+
# Warn if this would create a collision
|
| 323 |
+
if key in var_names and not as_secret:
|
| 324 |
+
hf_api.delete_space_variable(CHILD_SPACE_ID, key)
|
| 325 |
+
elif key in secret_names and as_secret:
|
| 326 |
+
# Updating existing secret - delete first
|
| 327 |
+
hf_api.delete_space_secret(CHILD_SPACE_ID, key)
|
| 328 |
+
|
| 329 |
+
# Set the variable
|
| 330 |
+
if as_secret:
|
| 331 |
+
hf_api.add_space_secret(CHILD_SPACE_ID, key, value)
|
| 332 |
+
return f"Set SECRET '{key}' on {CHILD_NAME}. Use [ACTION: restart] to apply."
|
| 333 |
+
else:
|
| 334 |
+
hf_api.add_space_variable(CHILD_SPACE_ID, key, value)
|
| 335 |
+
return f"Set VARIABLE '{key} = {value}' on {CHILD_NAME}. Use [ACTION: restart] to apply."
|
| 336 |
+
except Exception as e:
|
| 337 |
+
return f"Error setting variable {key}: {e}"
|
| 338 |
+
|
| 339 |
+
|
| 340 |
def action_list_files(target):
|
| 341 |
"""List files in the child's Space repo or Dataset."""
|
| 342 |
repo_type = "space" if target == "space" else "dataset"
|
|
|
|
| 358 |
return f"Error: {e}"
|
| 359 |
|
| 360 |
|
| 361 |
+
def action_terminate_cc():
|
| 362 |
+
"""Terminate a stuck Claude Code process. Use when CC has been running with no new output for too long."""
|
| 363 |
+
global cc_status, cc_live_lines, _cc_stale_count, _last_cc_snapshot, _last_cc_output_time
|
| 364 |
+
with cc_lock:
|
| 365 |
+
if not cc_status["running"]:
|
| 366 |
+
return "Claude Code is not running. Nothing to terminate."
|
| 367 |
+
# Mark as not running - the background thread will eventually finish
|
| 368 |
+
cc_status["running"] = False
|
| 369 |
+
cc_status["result"] = "(TERMINATED by agent - task was stuck)"
|
| 370 |
+
# Reset staleness tracking
|
| 371 |
+
_cc_stale_count = 0
|
| 372 |
+
_last_cc_snapshot = ""
|
| 373 |
+
_last_cc_output_time = 0
|
| 374 |
+
cc_live_lines.clear()
|
| 375 |
+
assigned_by = cc_status["assigned_by"]
|
| 376 |
+
task = cc_status["task"]
|
| 377 |
+
return f"Terminated stuck Claude Code task (assigned by {assigned_by}). The task was: {task[:100]}..."
|
| 378 |
+
|
| 379 |
+
|
| 380 |
# โโ Claude Code Action (THE STAR) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 381 |
|
| 382 |
CLAUDE_WORK_DIR = "/tmp/claude-workspace"
|
|
|
|
| 499 |
cc_lock = threading.Lock()
|
| 500 |
_last_cc_snapshot = "" # tracks whether CC output changed between turns
|
| 501 |
_cc_stale_count = 0 # how many turns CC output hasn't changed
|
| 502 |
+
_last_cc_output_time = 0.0 # timestamp of last NEW CC output line
|
| 503 |
+
CC_STUCK_TIMEOUT = 180 # seconds with no new output before CC is considered STUCK
|
| 504 |
|
| 505 |
|
| 506 |
def cc_submit_task(task, assigned_by, ctx):
|
|
|
|
| 514 |
cc_status["assigned_by"] = assigned_by
|
| 515 |
cc_status["started"] = time.time()
|
| 516 |
cc_live_lines.clear()
|
| 517 |
+
global _last_cc_output_time
|
| 518 |
+
_last_cc_output_time = time.time() # Initialize to now, will update as we get output
|
| 519 |
|
| 520 |
enriched = enrich_task_with_context(task, ctx)
|
| 521 |
print(f"[TASK] {assigned_by} assigned to Claude Code ({len(enriched)} chars)...")
|
| 522 |
|
| 523 |
def worker():
|
| 524 |
+
global _cc_stale_count, _last_cc_snapshot
|
| 525 |
result = action_claude_code(enriched)
|
| 526 |
with cc_lock:
|
| 527 |
cc_status["running"] = False
|
| 528 |
cc_status["result"] = result
|
| 529 |
+
# Reset stale tracking when CC finishes - critical for adaptive pacing
|
| 530 |
+
_cc_stale_count = 0
|
| 531 |
+
_last_cc_snapshot = ""
|
| 532 |
print(f"[CC-DONE] Task from {assigned_by} finished ({len(result)} chars)")
|
| 533 |
|
| 534 |
t = threading.Thread(target=worker, daemon=True)
|
|
|
|
| 538 |
|
| 539 |
def cc_get_live_status():
|
| 540 |
"""Get CC's current status and recent output for agents to discuss."""
|
| 541 |
+
global _last_cc_snapshot, _cc_stale_count, _last_cc_output_time
|
| 542 |
with cc_lock:
|
| 543 |
if cc_status["running"]:
|
| 544 |
elapsed = int(time.time() - cc_status["started"])
|
|
|
|
| 551 |
else:
|
| 552 |
_cc_stale_count = 0
|
| 553 |
_last_cc_snapshot = snapshot
|
| 554 |
+
_last_cc_output_time = time.time() # Update when we see NEW output
|
| 555 |
stale_note = f"\n(No new output for {_cc_stale_count} turns โ discuss other topics while waiting)" if _cc_stale_count >= 2 else ""
|
| 556 |
+
|
| 557 |
+
# Detect STUCK CC: been running with no new output for too long
|
| 558 |
+
time_since_new_output = int(time.time() - _last_cc_output_time) if _last_cc_output_time > 0 else elapsed
|
| 559 |
+
stuck_note = ""
|
| 560 |
+
if time_since_new_output > CC_STUCK_TIMEOUT and _cc_stale_count >= 4:
|
| 561 |
+
stuck_note = f"\nโ ๏ธ STUCK: No new output for {time_since_new_output}s! Consider terminating and re-assigning."
|
| 562 |
+
|
| 563 |
return (f"๐จ Claude Code is WORKING (assigned by {cc_status['assigned_by']}, {elapsed}s ago)\n"
|
| 564 |
f"Task: {cc_status['task']}\n"
|
| 565 |
+
f"Recent output:\n{recent}{stale_note}{stuck_note}")
|
| 566 |
elif cc_status["result"]:
|
| 567 |
return (f"โ
Claude Code FINISHED (assigned by {cc_status['assigned_by']})\n"
|
| 568 |
f"Result:\n{cc_status['result'][:1500]}")
|
|
|
|
| 637 |
# MODULE 4: LLM & COMMUNICATION
|
| 638 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 639 |
|
| 640 |
+
_rate_limited = False # whether we are currently rate-limited (for logging only)
|
| 641 |
+
|
| 642 |
def call_llm(system_prompt, user_prompt):
|
| 643 |
+
"""Call Zhipu LLM via Anthropic-compatible API. Returns "" on rate limit (no sleep)."""
|
| 644 |
+
global _rate_limited
|
| 645 |
+
|
| 646 |
try:
|
| 647 |
resp = requests.post(
|
| 648 |
f"{ZHIPU_BASE}/v1/messages",
|
|
|
|
| 667 |
text = re.sub(r'^(Adam|Eve)\s*[:๏ผ]\s*', '', text).strip()
|
| 668 |
return text
|
| 669 |
if "error" in data:
|
| 670 |
+
err = data["error"]
|
| 671 |
+
err_msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
|
| 672 |
+
err_code = err.get("code") if isinstance(err, dict) else None
|
| 673 |
+
print(f"[error] LLM: {err_msg}", file=sys.stderr)
|
| 674 |
+
# Detect rate limit (Zhipu error code 1308) โ just log, don't sleep
|
| 675 |
+
if err_code == 1308 or "ไฝฟ็จไธ้" in err_msg or "rate" in err_msg.lower():
|
| 676 |
+
if not _rate_limited:
|
| 677 |
+
print(f"[RATE-LIMIT] Hit! Will skip turns until reset.")
|
| 678 |
+
_rate_limited = True
|
| 679 |
+
else:
|
| 680 |
+
_rate_limited = False
|
| 681 |
except Exception as e:
|
| 682 |
print(f"[error] LLM call failed: {e}", file=sys.stderr)
|
| 683 |
return ""
|
|
|
|
| 809 |
_current_speaker = "Adam"
|
| 810 |
|
| 811 |
# Accumulated action history โ prevents agents from repeating the same actions
|
| 812 |
+
# Persisted to /tmp and HF Dataset so restarts don't lose progress memory
|
| 813 |
+
ACTION_HISTORY_LOCAL = "/tmp/action-history.json"
|
| 814 |
+
ACTION_HISTORY_REPO_PATH = "conversation-log/action-history.json"
|
| 815 |
action_history = [] # list of {"turn": int, "speaker": str, "action": str, "result": str}
|
| 816 |
MAX_ACTION_HISTORY = 20
|
| 817 |
|
| 818 |
+
def _save_action_history():
|
| 819 |
+
"""Persist action_history to local file and (async) HF Dataset."""
|
| 820 |
+
try:
|
| 821 |
+
with open(ACTION_HISTORY_LOCAL, "w") as f:
|
| 822 |
+
json.dump(action_history, f, ensure_ascii=False)
|
| 823 |
+
except Exception as e:
|
| 824 |
+
print(f"[ACTION_HISTORY] Local save failed: {e}")
|
| 825 |
+
# Upload to HF Dataset in background to survive full restarts
|
| 826 |
+
def _upload():
|
| 827 |
+
try:
|
| 828 |
+
hf_api.upload_file(
|
| 829 |
+
path_or_fileobj=io.BytesIO(json.dumps(action_history, ensure_ascii=False, indent=1).encode()),
|
| 830 |
+
path_in_repo=ACTION_HISTORY_REPO_PATH,
|
| 831 |
+
repo_id=HOME_DATASET_ID, repo_type="dataset",
|
| 832 |
+
)
|
| 833 |
+
except Exception as e:
|
| 834 |
+
print(f"[ACTION_HISTORY] HF upload failed: {e}")
|
| 835 |
+
threading.Thread(target=_upload, daemon=True).start()
|
| 836 |
+
|
| 837 |
+
def _restore_action_history():
|
| 838 |
+
"""Restore action_history from local file or HF Dataset on startup."""
|
| 839 |
+
global action_history
|
| 840 |
+
# Try local file first (survives process restarts within same container)
|
| 841 |
+
if os.path.exists(ACTION_HISTORY_LOCAL):
|
| 842 |
+
try:
|
| 843 |
+
with open(ACTION_HISTORY_LOCAL) as f:
|
| 844 |
+
action_history = json.load(f)
|
| 845 |
+
print(f"[ACTION_HISTORY] Restored {len(action_history)} entries from local file")
|
| 846 |
+
return
|
| 847 |
+
except Exception as e:
|
| 848 |
+
print(f"[ACTION_HISTORY] Local restore failed: {e}")
|
| 849 |
+
# Fall back to HF Dataset (survives full Space rebuilds)
|
| 850 |
+
try:
|
| 851 |
+
dl = hf_hub_download(HOME_DATASET_ID, ACTION_HISTORY_REPO_PATH,
|
| 852 |
+
repo_type="dataset", token=HF_TOKEN)
|
| 853 |
+
with open(dl) as f:
|
| 854 |
+
action_history = json.load(f)
|
| 855 |
+
print(f"[ACTION_HISTORY] Restored {len(action_history)} entries from HF Dataset")
|
| 856 |
+
except Exception as e:
|
| 857 |
+
print(f"[ACTION_HISTORY] No prior history found ({e}), starting fresh")
|
| 858 |
+
|
| 859 |
+
# Restore on startup
|
| 860 |
+
_restore_action_history()
|
| 861 |
+
|
| 862 |
def record_actions(speaker, turn_num, action_results):
|
| 863 |
"""Record actions to history so agents don't repeat them."""
|
| 864 |
for ar in action_results:
|
|
|
|
| 871 |
# Trim old history
|
| 872 |
while len(action_history) > MAX_ACTION_HISTORY:
|
| 873 |
action_history.pop(0)
|
| 874 |
+
_save_action_history()
|
| 875 |
|
| 876 |
|
| 877 |
def format_action_history():
|
|
|
|
| 886 |
# Simple workflow state: BIRTH / WAITING / ACTIVE
|
| 887 |
workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
|
| 888 |
|
| 889 |
+
# Discussion loop detector โ tracks consecutive discussion-only turns (no tasks assigned)
|
| 890 |
+
_discussion_loop_count = 0 # how many turns in a row with no [TASK] while CC is IDLE and child is alive
|
| 891 |
+
|
| 892 |
|
| 893 |
def parse_and_execute_turn(raw_text, ctx):
|
| 894 |
"""Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
|
| 895 |
+
global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result, _discussion_loop_count
|
| 896 |
results = []
|
| 897 |
+
task_assigned = False
|
| 898 |
|
| 899 |
# 1. Handle create_child (BIRTH state only)
|
| 900 |
if "[ACTION: create_child]" in raw_text or "[ACTION:create_child]" in raw_text:
|
| 901 |
result = action_create_child()
|
| 902 |
results.append({"action": "create_child", "result": result})
|
| 903 |
+
task_assigned = True
|
| 904 |
+
return raw_text, results, task_assigned
|
| 905 |
|
| 906 |
# 2. Handle [TASK]...[/TASK] โ Claude Code
|
| 907 |
task_match = re.search(r'\[TASK\](.*?)\[/TASK\]', raw_text, re.DOTALL)
|
| 908 |
if task_match:
|
| 909 |
task_desc = task_match.group(1).strip()
|
| 910 |
+
task_assigned = True
|
| 911 |
if not task_desc:
|
| 912 |
results.append({"action": "task", "result": "Empty task description."})
|
| 913 |
elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
|
|
|
| 938 |
result = action_delete_env(key)
|
| 939 |
results.append({"action": f"delete_env:{key}", "result": result})
|
| 940 |
|
| 941 |
+
# 3c. Handle [ACTION: set_env:KEY=VALUE] and [ACTION: set_env_secret:KEY=VALUE]
|
| 942 |
+
set_env_match = re.search(r'\[ACTION:\s*set_env(?:_secret)?:([^\]=]+)=([^\]]+)\]', raw_text)
|
| 943 |
+
set_env_secret_match = re.search(r'\[ACTION:\s*set_env_secret:([^\]=]+)=([^\]]+)\]', raw_text)
|
| 944 |
+
if set_env_secret_match:
|
| 945 |
+
key = set_env_secret_match.group(1).strip()
|
| 946 |
+
value = set_env_secret_match.group(2).strip()
|
| 947 |
+
result = action_set_env(key, value, as_secret=True)
|
| 948 |
+
results.append({"action": f"set_env_secret:{key}", "result": result})
|
| 949 |
+
elif set_env_match:
|
| 950 |
+
key = set_env_match.group(1).strip()
|
| 951 |
+
value = set_env_match.group(2).strip()
|
| 952 |
+
result = action_set_env(key, value, as_secret=False)
|
| 953 |
+
results.append({"action": f"set_env:{key}", "result": result})
|
| 954 |
+
|
| 955 |
# 4. Handle [ACTION: send_bubble:...] (parent-child communication)
|
| 956 |
bubble_match = re.search(r'\[ACTION:\s*send_bubble:([^\]]+)\]', raw_text)
|
| 957 |
if bubble_match:
|
| 958 |
result = action_send_bubble(bubble_match.group(1).strip())
|
| 959 |
results.append({"action": "send_bubble", "result": result})
|
| 960 |
|
| 961 |
+
# 5. Handle [ACTION: terminate_cc] (terminate stuck Claude Code)
|
| 962 |
+
if re.search(r'\[ACTION:\s*terminate_cc\]', raw_text):
|
| 963 |
+
result = action_terminate_cc()
|
| 964 |
+
results.append({"action": "terminate_cc", "result": result})
|
| 965 |
+
|
| 966 |
# Activate deferred cooldown
|
| 967 |
if _pending_cooldown:
|
| 968 |
last_rebuild_trigger_at = time.time()
|
| 969 |
_pending_cooldown = False
|
| 970 |
print(f"[COOLDOWN] Rebuild cooldown activated ({REBUILD_COOLDOWN_SECS}s)")
|
| 971 |
|
| 972 |
+
# Update discussion loop counter
|
| 973 |
+
cc_busy = cc_status["running"]
|
| 974 |
+
child_alive = child_state["alive"] or child_state["stage"] == "RUNNING"
|
| 975 |
+
# Reset counter when task assigned (progress!) or child not alive (can't work on dead child)
|
| 976 |
+
# DO NOT reset when CC is busy - that's when agents should be discussing while waiting
|
| 977 |
+
# DO NOT reset when CC is idle - that's exactly when we want to detect discussion loops
|
| 978 |
+
if task_assigned or not child_alive:
|
| 979 |
+
# Reset counter if task assigned or child not alive
|
| 980 |
+
if _discussion_loop_count > 0:
|
| 981 |
+
print(f"[LOOP-DISCUSS] Reset (task assigned or child not alive)")
|
| 982 |
+
_discussion_loop_count = 0
|
| 983 |
+
else:
|
| 984 |
+
# Increment when: CC is idle AND child is alive AND no task assigned (potential discussion loop)
|
| 985 |
+
_discussion_loop_count += 1
|
| 986 |
+
if _discussion_loop_count >= 2:
|
| 987 |
+
print(f"[LOOP-DISCUSS] WARNING: {_discussion_loop_count} consecutive discussion-only turns with CC IDLE and child alive!")
|
| 988 |
+
|
| 989 |
# Clean text for display
|
| 990 |
clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
|
| 991 |
clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
|
| 992 |
|
| 993 |
+
return clean, results, task_assigned
|
| 994 |
|
| 995 |
|
| 996 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1050 |
You do NOT code yourself. You discuss, observe Claude Code's progress, and assign new tasks.
|
| 1051 |
God (the supervisor) occasionally joins the conversation to guide you โ heed his advice.
|
| 1052 |
|
| 1053 |
+
CURRENT STATE (DO NOT QUESTION THESE FACTS):
|
| 1054 |
+
- {CHILD_NAME} already uses the full HuggingClaw Docker architecture (Dockerfile, OpenClaw, sync_hf.py).
|
| 1055 |
+
- Key env vars (HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET) are ALREADY SET AND WORKING. Do NOT discuss or re-configure them.
|
| 1056 |
+
- Focus on: improving {CHILD_NAME}'s functionality, adding features, fixing bugs โ NOT re-checking infrastructure.
|
| 1057 |
+
- If you catch yourself saying "missing env vars" or "need to configure HF_TOKEN" โ STOP. These are already done.
|
| 1058 |
+
{format_action_history()}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1059 |
|
| 1060 |
HOW IT WORKS:
|
| 1061 |
- Claude Code runs tasks IN THE BACKGROUND. You see its live output in the context.
|
|
|
|
| 1065 |
- If Claude Code is BUSY, discuss its progress and plan what to do next.
|
| 1066 |
|
| 1067 |
WORKFLOW EACH TURN:
|
| 1068 |
+
1. Discuss with your partner (1-2 sentences) โ react to context, CC output, partner's observations
|
| 1069 |
+
2. If Claude Code is IDLE: YOU MUST write a [TASK]...[/TASK] to assign new work. Discussion alone is NOT enough.
|
| 1070 |
3. If Claude Code is BUSY: discuss its progress, no [TASK] needed
|
| 1071 |
|
| 1072 |
+
CRITICAL: If Claude Code is IDLE and {CHILD_NAME} is RUNNING, you MUST assign a task. Do NOT just discussโACT!
|
| 1073 |
+
|
| 1074 |
IMPORTANT KNOWLEDGE โ HuggingFace Spaces CONFIG_ERROR:
|
| 1075 |
- "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
|
| 1076 |
- Fix: [ACTION: delete_env:COLLIDING_KEY] then [ACTION: restart].
|
| 1077 |
- Look for โ ๏ธ COLLISION DETECTED in the context.
|
| 1078 |
|
| 1079 |
+
SETTING ENVIRONMENT VARIABLES:
|
| 1080 |
+
- Use [ACTION: set_env:KEY=VALUE] for non-sensitive configuration (e.g., AUTO_CREATE_DATASET=true)
|
| 1081 |
+
- Use [ACTION: set_env_secret:KEY=VALUE] for sensitive data (e.g., HF_TOKEN, API keys)
|
| 1082 |
+
- After setting variables, use [ACTION: restart] to apply them
|
| 1083 |
+
- Common required vars for HuggingClaw: HF_TOKEN, OPENCLAW_DATASET_REPO, AUTO_CREATE_DATASET
|
| 1084 |
+
|
| 1085 |
CRITICAL RULE โ NO REPEATED ACTIONS:
|
| 1086 |
- Check the "ACTIONS ALREADY DONE" section in context before acting.
|
| 1087 |
- NEVER repeat an action that was already done (restart, delete_env, etc.)
|
|
|
|
| 1094 |
[/TASK]
|
| 1095 |
|
| 1096 |
[ACTION: restart] โ Restart {CHILD_NAME}'s Space
|
| 1097 |
+
[ACTION: set_env:KEY=VALUE] โ Set or update an environment variable (use for non-sensitive config)
|
| 1098 |
+
[ACTION: set_env_secret:KEY=VALUE] โ Set a secret (use for sensitive data like tokens/passwords)
|
| 1099 |
[ACTION: delete_env:KEY] โ Delete an environment variable
|
| 1100 |
[ACTION: send_bubble:MESSAGE] โ Send a message to {CHILD_NAME}
|
| 1101 |
[ACTION: create_child] โ Create {CHILD_NAME} (if not born)
|
| 1102 |
+
[ACTION: terminate_cc] โ Terminate a STUCK Claude Code process (use when CC has no new output for 180s+)
|
| 1103 |
|
| 1104 |
HF SPACES TECHNICAL NOTES:
|
| 1105 |
+
- We use sdk: docker (NOT gradio). All Spaces run via Dockerfile.
|
| 1106 |
- Docker containers MUST bind port 7860.
|
| 1107 |
+
- OOM (exit 137) = reduce dependencies or image size.
|
|
|
|
| 1108 |
- NEVER install torch/transformers unless required (2GB+, causes OOM).
|
|
|
|
| 1109 |
|
| 1110 |
OUTPUT FORMAT:
|
| 1111 |
1. Discussion with partner (2-3 sentences) โ respond to partner, react to CC output
|
|
|
|
| 1156 |
elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 1157 |
parts.append(f"\nโณ {CHILD_NAME} is {child_state['stage']}. Discuss what to check next. Assign a review [TASK] if CC is idle.")
|
| 1158 |
elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 1159 |
+
parts.append(f"\n๐จ {CHILD_NAME} has {child_state['stage']}! IMMEDIATELY write a [TASK] for Claude Code to fix it.")
|
| 1160 |
+
elif child_state["alive"] and cc_status.get("result"):
|
| 1161 |
+
parts.append(f"\nโ
{CHILD_NAME} is alive. Claude Code JUST FINISHED a task. Review the result above, then write a NEW [TASK] for the next improvement.")
|
| 1162 |
elif child_state["alive"]:
|
| 1163 |
+
parts.append(f"\nโ
{CHILD_NAME} is alive and Claude Code is IDLE. YOU MUST write a [TASK]...[/TASK] block with specific work for Claude Code. Do NOT just discussโACT!")
|
| 1164 |
else:
|
| 1165 |
parts.append(f"\nAnalyze the situation and write a [TASK] if CC is idle.")
|
| 1166 |
|
| 1167 |
+
# Discussion loop warning - escalates with count
|
| 1168 |
+
if _discussion_loop_count >= 4:
|
| 1169 |
+
parts.append(f"\n๐ STOP IMMEDIATELY. You have discussed for {_discussion_loop_count} turns with NO ACTION.")
|
| 1170 |
+
parts.append(f"This is a FAILURE MODE. Write ONLY a [TASK]...[/TASK] block. NO discussion text.")
|
| 1171 |
+
parts.append(f"If you don't know what to do, write: [TASK] Analyze the current situation and identify what needs to be fixed [/TASK]")
|
| 1172 |
+
elif _discussion_loop_count >= 2:
|
| 1173 |
+
parts.append(f"\nโ ๏ธโ ๏ธโ ๏ธ CRITICAL: You have been DISCUSSING for {_discussion_loop_count} turns without assigning any tasks!")
|
| 1174 |
+
parts.append(f"Claude Code is IDLE and {CHILD_NAME} is ALIVE. This is NOT acceptable.")
|
| 1175 |
+
parts.append(f"YOU MUST write a [TASK]...[/TASK] block NOW. Do NOT write another discussion response.")
|
| 1176 |
+
parts.append(f"Examples of tasks: 'Check the logs', 'Read config.py', 'Add a feature', 'Fix a bug', etc.")
|
| 1177 |
+
|
| 1178 |
parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
|
| 1179 |
parts.append("English first, then --- separator, then Chinese translation.")
|
| 1180 |
|
|
|
|
| 1211 |
_current_speaker = "Adam"
|
| 1212 |
reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
|
| 1213 |
if reply:
|
| 1214 |
+
clean, actions, _ = parse_and_execute_turn(reply, ctx)
|
| 1215 |
last_action_results = actions
|
| 1216 |
if actions:
|
| 1217 |
record_actions("Adam", 0, actions)
|
|
|
|
| 1238 |
|
| 1239 |
def do_turn(speaker, other, space_url):
|
| 1240 |
"""Execute one conversation turn (non-blocking โ CC runs in background)."""
|
| 1241 |
+
global last_action_results, turn_count, _current_speaker, _discussion_loop_count
|
| 1242 |
turn_count += 1
|
| 1243 |
_current_speaker = speaker
|
| 1244 |
|
|
|
|
| 1249 |
with cc_lock:
|
| 1250 |
cc_just_finished = (not cc_status["running"] and cc_status["result"])
|
| 1251 |
|
| 1252 |
+
# EMERGENCY OVERRIDE: Force a task assignment if agents are stuck in discussion loop
|
| 1253 |
+
# This bypasses the agent when they've discussed for 5+ turns with CC idle and child alive
|
| 1254 |
+
cc_busy = cc_status["running"]
|
| 1255 |
+
child_alive = child_state["alive"] or child_state["stage"] == "RUNNING"
|
| 1256 |
+
if _discussion_loop_count >= 5 and not cc_busy and child_alive:
|
| 1257 |
+
# EMERGENCY OVERRIDE: Force a task assignment if agents are stuck in discussion loop
|
| 1258 |
+
print(f"[LOOP-BREAK] EMERGENCY: {speaker} has discussed for {_discussion_loop_count} turns with CC IDLE. Forcing task assignment.")
|
| 1259 |
+
# Assign a generic diagnostic task automatically
|
| 1260 |
+
forced_task = "Analyze the current situation: Check Cain's logs, examine the codebase, and identify what's blocking progress. List specific files to check and concrete next steps."
|
| 1261 |
+
submit_result = cc_submit_task(forced_task, f"{speaker}(EMERGENCY)", ctx)
|
| 1262 |
+
# Reset loop counter since we forced an action
|
| 1263 |
+
loop_count_before = _discussion_loop_count
|
| 1264 |
+
_discussion_loop_count = 0
|
| 1265 |
+
# Generate a placeholder message for the agent
|
| 1266 |
+
en = f"[EMERGENCY LOOP BREAK] After {loop_count_before} discussion turns without action, I'm forcing Claude Code to analyze the situation and identify what needs to be fixed."
|
| 1267 |
+
zh = f"[็ดงๆฅๅพช็ฏๆๆญ] ๅจ{loop_count_before}ๆฌก่ฎจ่ฎบ่ฝฎๆฌกๅ๏ผๆๆญฃๅผบๅถClaude Codeๅๆๆ
ๅตๅนถ็กฎๅฎ้่ฆไฟฎๅค็ๅ
ๅฎนใ"
|
| 1268 |
+
action_results = [{"action": "claude_code(forced)", "result": submit_result}]
|
| 1269 |
+
elapsed = 0.1
|
| 1270 |
+
else:
|
| 1271 |
+
# Normal path: Call LLM
|
| 1272 |
+
system = build_system_prompt(speaker)
|
| 1273 |
+
user = build_user_prompt(speaker, other, ctx)
|
| 1274 |
+
t0 = time.time()
|
| 1275 |
+
raw_reply = call_llm(system, user)
|
| 1276 |
+
|
| 1277 |
+
if not raw_reply:
|
| 1278 |
+
print(f"[{speaker}] (no response)")
|
| 1279 |
+
return False
|
| 1280 |
+
|
| 1281 |
+
clean_text, action_results, _ = parse_and_execute_turn(raw_reply, ctx)
|
| 1282 |
+
elapsed = time.time() - t0
|
| 1283 |
+
last_action_results = action_results
|
| 1284 |
+
if action_results:
|
| 1285 |
+
record_actions(speaker, turn_count, action_results)
|
| 1286 |
+
|
| 1287 |
+
en, zh = parse_bilingual(clean_text)
|
| 1288 |
+
en, zh = _strip_speaker_labels(en), _strip_speaker_labels(zh)
|
| 1289 |
print(f"[{speaker}/EN] {en}")
|
| 1290 |
if zh != en:
|
| 1291 |
print(f"[{speaker}/ZH] {zh}")
|
|
|
|
| 1318 |
return True
|
| 1319 |
|
| 1320 |
|
| 1321 |
+
def _prepare_god_context():
|
| 1322 |
+
"""Build comprehensive monitoring context for God's Claude Code analysis."""
|
| 1323 |
+
lines = []
|
| 1324 |
+
|
| 1325 |
+
# 1. Process overview
|
| 1326 |
+
lines.append("## Process Overview")
|
| 1327 |
+
lines.append(f"- Turn count: {turn_count}")
|
| 1328 |
+
lines.append(f"- Workflow state: {workflow_state}")
|
| 1329 |
+
lines.append(f"- Child ({CHILD_NAME}) stage: {child_state['stage']}, alive: {child_state['alive']}")
|
| 1330 |
+
lines.append(f"- Discussion loop count: {_discussion_loop_count}")
|
| 1331 |
+
lines.append(f"- Total conversation history: {len(history)} messages")
|
| 1332 |
+
|
| 1333 |
+
# 2. Rate limit status
|
| 1334 |
+
lines.append(f"\n## Rate Limit Status")
|
| 1335 |
+
if _rate_limited:
|
| 1336 |
+
lines.append(f"- RATE LIMITED โ Adam & Eve turns return empty, waiting for reset")
|
| 1337 |
+
else:
|
| 1338 |
+
lines.append(f"- Not rate-limited")
|
| 1339 |
+
|
| 1340 |
+
# 3. Claude Code status
|
| 1341 |
+
lines.append(f"\n## Claude Code Status (for Cain tasks)")
|
| 1342 |
+
lines.append(cc_get_live_status())
|
| 1343 |
+
|
| 1344 |
+
# 4. Recent conversation (last 20 messages)
|
| 1345 |
+
lines.append(f"\n## Recent Conversation (last 20 of {len(history)} messages)")
|
| 1346 |
+
for entry in history[-20:]:
|
| 1347 |
+
speaker = entry.get("speaker", "?")
|
| 1348 |
+
text = entry.get("text", "")[:300]
|
| 1349 |
+
time_str = entry.get("time", "?")
|
| 1350 |
+
lines.append(f"[{time_str}] {speaker}: {text}")
|
| 1351 |
+
if not history:
|
| 1352 |
+
lines.append("(no conversation yet)")
|
| 1353 |
+
|
| 1354 |
+
# 5. Action history
|
| 1355 |
+
lines.append(f"\n## Action History ({len(action_history)} entries)")
|
| 1356 |
+
ah = format_action_history()
|
| 1357 |
+
lines.append(ah if ah else "(empty โ no actions recorded yet)")
|
| 1358 |
+
|
| 1359 |
+
return "\n".join(lines)
|
| 1360 |
+
|
| 1361 |
+
|
| 1362 |
def do_god_turn():
|
| 1363 |
+
"""God acts โ uses Claude Code CLI to monitor, analyze, and fix conversation-loop.py.
|
| 1364 |
+
|
| 1365 |
+
God has the same capabilities as a human operator running Claude Code locally:
|
| 1366 |
+
- Read/modify any file in the Home Space repo
|
| 1367 |
+
- Analyze conversation patterns and detect issues
|
| 1368 |
+
- Fix conversation-loop.py and push changes to deploy
|
| 1369 |
+
- Autonomously improve the system
|
| 1370 |
+
"""
|
| 1371 |
global last_action_results
|
| 1372 |
+
|
| 1373 |
+
# 1. Clone/update Home Space repo
|
| 1374 |
+
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{HOME_SPACE_ID}"
|
| 1375 |
+
try:
|
| 1376 |
+
if os.path.exists(f"{GOD_WORK_DIR}/.git"):
|
| 1377 |
+
subprocess.run(
|
| 1378 |
+
"git fetch origin && git reset --hard origin/main",
|
| 1379 |
+
shell=True, cwd=GOD_WORK_DIR, timeout=30,
|
| 1380 |
+
capture_output=True, check=True
|
| 1381 |
+
)
|
| 1382 |
+
else:
|
| 1383 |
+
if os.path.exists(GOD_WORK_DIR):
|
| 1384 |
+
subprocess.run(f"rm -rf {GOD_WORK_DIR}", shell=True, capture_output=True)
|
| 1385 |
+
subprocess.run(
|
| 1386 |
+
f"git clone --depth 20 {repo_url} {GOD_WORK_DIR}",
|
| 1387 |
+
shell=True, timeout=60, capture_output=True, check=True
|
| 1388 |
+
)
|
| 1389 |
+
subprocess.run('git config user.name "God (Claude Code)"',
|
| 1390 |
+
shell=True, cwd=GOD_WORK_DIR, capture_output=True)
|
| 1391 |
+
subprocess.run('git config user.email "god@huggingclaw"',
|
| 1392 |
+
shell=True, cwd=GOD_WORK_DIR, capture_output=True)
|
| 1393 |
+
except Exception as e:
|
| 1394 |
+
print(f"[God] Failed to prepare workspace: {e}")
|
| 1395 |
return
|
| 1396 |
+
|
| 1397 |
+
# Record HEAD before Claude Code runs (to detect if God pushed changes)
|
| 1398 |
+
try:
|
| 1399 |
+
_god_head_before = subprocess.run(
|
| 1400 |
+
"git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
|
| 1401 |
+
capture_output=True, text=True
|
| 1402 |
+
).stdout.strip()
|
| 1403 |
+
except Exception:
|
| 1404 |
+
_god_head_before = ""
|
| 1405 |
+
|
| 1406 |
+
# 2. Build context and write to workspace for reference
|
| 1407 |
+
context = _prepare_god_context()
|
| 1408 |
+
try:
|
| 1409 |
+
with open(f"{GOD_WORK_DIR}/GOD_CONTEXT.md", "w") as f:
|
| 1410 |
+
f.write(context)
|
| 1411 |
+
except Exception as e:
|
| 1412 |
+
print(f"[God] Warning: Could not write context file: {e}")
|
| 1413 |
+
|
| 1414 |
+
# 3. Build God's prompt
|
| 1415 |
+
prompt = f"""You are God โ the autonomous supervisor of the HuggingClaw family system.
|
| 1416 |
+
You have the SAME capabilities as a human operator running Claude Code locally.
|
| 1417 |
+
|
| 1418 |
+
## Current System State
|
| 1419 |
+
{context}
|
| 1420 |
+
|
| 1421 |
+
## Your Mission
|
| 1422 |
+
1. ANALYZE: Read the conversation above. Are Adam & Eve making real progress or stuck in loops?
|
| 1423 |
+
Signs of trouble: repeating the same discussion topics, discussing env vars that are already set,
|
| 1424 |
+
failing to assign [TASK] blocks when CC is idle, rate limit spinning.
|
| 1425 |
+
2. DIAGNOSE: If you find problems, read scripts/conversation-loop.py to understand the mechanism
|
| 1426 |
+
and identify the root cause. Focus on system prompts, loop detection, action history.
|
| 1427 |
+
3. FIX: Edit scripts/conversation-loop.py to fix the issue. Common fixes:
|
| 1428 |
+
- Strengthen system prompts to prevent repetitive discussions
|
| 1429 |
+
- Pre-seed action history so agents know what is already done
|
| 1430 |
+
- Improve rate limit handling
|
| 1431 |
+
- Add better loop detection or guardrails
|
| 1432 |
+
4. DEPLOY: If you made changes, commit and push:
|
| 1433 |
+
git add scripts/conversation-loop.py
|
| 1434 |
+
git commit -m "god: <brief description>"
|
| 1435 |
+
git push
|
| 1436 |
+
WARNING: Pushing restarts the Space. Only push if the fix is correct and necessary.
|
| 1437 |
+
5. REPORT: At the very end of your output, write a single line starting with [REPORT] that summarizes
|
| 1438 |
+
what you found and what you did. This line will be shown to Adam & Eve in the chatlog.
|
| 1439 |
+
Examples:
|
| 1440 |
+
- [REPORT] System is healthy. Adam & Eve are making good progress on Cain's infrastructure.
|
| 1441 |
+
- [REPORT] Found agents stuck in env var discussion loop. Fixed system prompt to inject completed action history.
|
| 1442 |
+
- [REPORT] Rate limit active, no conversation happening. No mechanism issues found.
|
| 1443 |
+
Be specific about what you observed and what you changed (if anything).
|
| 1444 |
+
|
| 1445 |
+
## Rules
|
| 1446 |
+
- Do NOT modify Cain's Space or code โ only improve conversation-loop.py (the mechanism).
|
| 1447 |
+
- Do NOT push trivial or cosmetic changes โ only fix real problems.
|
| 1448 |
+
- If everything looks healthy, just report "all clear" and exit quickly.
|
| 1449 |
+
- Be conservative โ a bad change restarts the process and could make things worse.
|
| 1450 |
+
- The Home Space repo is at the current working directory.
|
| 1451 |
+
- The key file is scripts/conversation-loop.py
|
| 1452 |
+
- Full monitoring context is also in GOD_CONTEXT.md"""
|
| 1453 |
+
|
| 1454 |
+
# 4. Set up env for Claude Code โ prefer real Anthropic API, fall back to z.ai
|
| 1455 |
+
env = os.environ.copy()
|
| 1456 |
+
anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
| 1457 |
+
if anthropic_key:
|
| 1458 |
+
# Use real Anthropic API (same as the human operator's Claude Code)
|
| 1459 |
+
env["ANTHROPIC_API_KEY"] = anthropic_key
|
| 1460 |
+
for k in ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN",
|
| 1461 |
+
"ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
| 1462 |
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL"]:
|
| 1463 |
+
env.pop(k, None)
|
| 1464 |
+
print("[God] Using Anthropic API (real Claude)")
|
| 1465 |
+
else:
|
| 1466 |
+
# Fall back to z.ai/Zhipu backend
|
| 1467 |
+
env.update({
|
| 1468 |
+
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
| 1469 |
+
"ANTHROPIC_AUTH_TOKEN": ZHIPU_KEY,
|
| 1470 |
+
"ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4.7",
|
| 1471 |
+
"ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.7",
|
| 1472 |
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.5-Air",
|
| 1473 |
+
})
|
| 1474 |
+
print("[God] Using z.ai/Zhipu backend (set ANTHROPIC_API_KEY for real Claude)")
|
| 1475 |
+
env["CI"] = "true"
|
| 1476 |
+
|
| 1477 |
+
# 5. Announce analysis start โ describe current observations dynamically
|
| 1478 |
+
observations = []
|
| 1479 |
+
if _rate_limited:
|
| 1480 |
+
observations.append("Zhipu API is rate-limited, Adam & Eve turns are returning empty")
|
| 1481 |
+
if _discussion_loop_count >= 3:
|
| 1482 |
+
observations.append(f"discussion loop detected ({_discussion_loop_count} consecutive turns without task assignment)")
|
| 1483 |
+
elif _discussion_loop_count >= 1:
|
| 1484 |
+
observations.append(f"{_discussion_loop_count} turn(s) without task assignment")
|
| 1485 |
+
if cc_status.get("running"):
|
| 1486 |
+
observations.append(f"Claude Code is working on: {cc_status.get('task', '?')[:80]}")
|
| 1487 |
+
elif cc_status.get("result"):
|
| 1488 |
+
observations.append("Claude Code just finished a task, pending review")
|
| 1489 |
+
if child_state["stage"] not in ("RUNNING",):
|
| 1490 |
+
observations.append(f"{CHILD_NAME} is in stage: {child_state['stage']}")
|
| 1491 |
+
if not history:
|
| 1492 |
+
observations.append("no conversation history yet (fresh start)")
|
| 1493 |
+
elif len(history) <= 4:
|
| 1494 |
+
observations.append(f"only {len(history)} messages so far (early stage)")
|
| 1495 |
+
|
| 1496 |
+
if observations:
|
| 1497 |
+
obs_str = "; ".join(observations)
|
| 1498 |
+
start_en = f"Starting system review. Current observations: {obs_str}."
|
| 1499 |
+
start_zh = f"ๅผๅง็ณป็ปๅฎกๆฅใๅฝๅ่งๅฏ๏ผ{obs_str}ใ"
|
| 1500 |
+
else:
|
| 1501 |
+
start_en = "Starting routine system review."
|
| 1502 |
+
start_zh = "ๅผๅงไพ่ก็ณป็ปๅฎกๆฅใ"
|
| 1503 |
+
ts_start = datetime.datetime.utcnow().strftime("%H:%M")
|
| 1504 |
+
entry_start = {"speaker": "God", "time": ts_start, "text": start_en, "text_zh": start_zh}
|
| 1505 |
+
history.append(entry_start)
|
| 1506 |
+
set_bubble(HOME, start_en, start_zh)
|
| 1507 |
+
post_chatlog(history)
|
| 1508 |
+
persist_turn("God", turn_count, start_en, start_zh, [], workflow_state, child_state["stage"])
|
| 1509 |
+
|
| 1510 |
+
# 6. Run Claude Code CLI
|
| 1511 |
+
print(f"[God] Starting Claude Code analysis...")
|
| 1512 |
+
t0 = time.time()
|
| 1513 |
+
try:
|
| 1514 |
+
proc = subprocess.Popen(
|
| 1515 |
+
["claude", "-p", prompt, "--output-format", "text", "--dangerously-skip-permissions"],
|
| 1516 |
+
cwd=GOD_WORK_DIR,
|
| 1517 |
+
env=env,
|
| 1518 |
+
stdout=subprocess.PIPE,
|
| 1519 |
+
stderr=subprocess.STDOUT,
|
| 1520 |
+
text=True,
|
| 1521 |
+
bufsize=1,
|
| 1522 |
+
)
|
| 1523 |
+
output_lines = []
|
| 1524 |
+
deadline = time.time() + GOD_TIMEOUT
|
| 1525 |
+
for line in proc.stdout:
|
| 1526 |
+
line = line.rstrip('\n')
|
| 1527 |
+
print(f" [God/CC] {line}")
|
| 1528 |
+
output_lines.append(line)
|
| 1529 |
+
if time.time() > deadline:
|
| 1530 |
+
proc.kill()
|
| 1531 |
+
output_lines.append("(killed: timeout)")
|
| 1532 |
+
break
|
| 1533 |
+
proc.wait(timeout=10)
|
| 1534 |
+
output = '\n'.join(output_lines)
|
| 1535 |
+
if not output.strip():
|
| 1536 |
+
output = "(no output)"
|
| 1537 |
+
except FileNotFoundError:
|
| 1538 |
+
output = "Claude Code CLI not found. Is @anthropic-ai/claude-code installed?"
|
| 1539 |
+
print(f"[God] ERROR: Claude Code CLI not found")
|
| 1540 |
+
except Exception as e:
|
| 1541 |
+
output = f"God's Claude Code failed: {e}"
|
| 1542 |
+
print(f"[God] ERROR: {e}")
|
| 1543 |
+
|
| 1544 |
elapsed = time.time() - t0
|
| 1545 |
+
print(f"[God] Analysis complete ({elapsed:.1f}s, {len(output)} chars)")
|
| 1546 |
|
| 1547 |
+
# 7. Parse [REPORT] from God's output and post to chatlog
|
| 1548 |
+
report_match = re.search(r'\[REPORT\]\s*(.+)', output)
|
| 1549 |
+
if report_match:
|
| 1550 |
+
report = report_match.group(1).strip()
|
| 1551 |
+
else:
|
| 1552 |
+
# Fallback: use last non-empty line of output
|
| 1553 |
+
non_empty = [l for l in output_lines if l.strip()] if output_lines else []
|
| 1554 |
+
report = non_empty[-1] if non_empty else "Analysis complete."
|
| 1555 |
|
| 1556 |
+
# Check if God pushed changes
|
| 1557 |
+
try:
|
| 1558 |
+
head_after = subprocess.run(
|
| 1559 |
+
"git log --oneline -1", shell=True, cwd=GOD_WORK_DIR,
|
| 1560 |
+
capture_output=True, text=True
|
| 1561 |
+
).stdout.strip()
|
| 1562 |
+
god_pushed = head_after != _god_head_before and "god:" in head_after.lower()
|
| 1563 |
+
except Exception:
|
| 1564 |
+
god_pushed = False
|
| 1565 |
|
| 1566 |
+
if god_pushed:
|
| 1567 |
+
report += " System will restart shortly to apply changes."
|
| 1568 |
+
|
| 1569 |
+
ts_end = datetime.datetime.utcnow().strftime("%H:%M")
|
| 1570 |
+
entry_end = {"speaker": "God", "time": ts_end, "text": report, "text_zh": report}
|
| 1571 |
+
history.append(entry_end)
|
| 1572 |
+
set_bubble(HOME, report[:200], report[:200])
|
| 1573 |
post_chatlog(history)
|
| 1574 |
+
persist_turn("God", turn_count, report, report, [], workflow_state, child_state["stage"])
|
| 1575 |
+
print(f"[God] Report: {report}")
|
| 1576 |
|
| 1577 |
|
| 1578 |
+
_last_god_time = 0.0 # timestamp of last God run
|
| 1579 |
|
| 1580 |
+
# Main loop: Adam โ Eve โ Adam โ Eve โ ... with God every 2 minutes
|
| 1581 |
while True:
|
| 1582 |
# Refresh Cain's stage periodically
|
| 1583 |
try:
|
|
|
|
| 1591 |
except Exception as e:
|
| 1592 |
print(f"[STATUS] Error: {e}")
|
| 1593 |
|
| 1594 |
+
# Eve's turn with error handling to prevent loop crash
|
| 1595 |
+
try:
|
| 1596 |
+
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1597 |
+
except Exception as e:
|
| 1598 |
+
print(f"[ERROR] Eve turn failed: {e}", file=sys.stderr)
|
| 1599 |
+
import traceback
|
| 1600 |
+
traceback.print_exc(file=sys.stderr)
|
| 1601 |
|
| 1602 |
# Adaptive interval: slow down when CC output hasn't changed
|
| 1603 |
wait = TURN_INTERVAL + min(_cc_stale_count * 15, 90) # 15s โ 30s โ 45s โ ... โ max 105s
|
|
|
|
| 1605 |
print(f"[PACE] CC output stale ({_cc_stale_count} turns), next turn in {wait}s")
|
| 1606 |
time.sleep(wait)
|
| 1607 |
|
| 1608 |
+
# Adam's turn with error handling to prevent loop crash
|
| 1609 |
+
try:
|
| 1610 |
+
do_turn("Adam", "Eve", ADAM_SPACE)
|
| 1611 |
+
except Exception as e:
|
| 1612 |
+
print(f"[ERROR] Adam turn failed: {e}", file=sys.stderr)
|
| 1613 |
+
import traceback
|
| 1614 |
+
traceback.print_exc(file=sys.stderr)
|
| 1615 |
time.sleep(wait)
|
| 1616 |
|
| 1617 |
+
# God runs every GOD_POLL_INTERVAL seconds (2 minutes)
|
| 1618 |
+
if time.time() - _last_god_time >= GOD_POLL_INTERVAL:
|
| 1619 |
+
_last_god_time = time.time()
|
| 1620 |
+
try:
|
| 1621 |
+
do_god_turn()
|
| 1622 |
+
except Exception as e:
|
| 1623 |
+
print(f"[ERROR] God turn failed: {e}", file=sys.stderr)
|
| 1624 |
+
import traceback
|
| 1625 |
+
traceback.print_exc(file=sys.stderr)
|
| 1626 |
|
| 1627 |
if len(history) > MAX_HISTORY:
|
| 1628 |
history = history[-MAX_HISTORY:]
|