Spaces:
Paused
refactor: conversation-loop v2 — Claude Code as primary executor
Browse filesComplete architecture change:
- OLD: GLM picks from 15+ actions, manually reads files/writes code (badly)
- NEW: GLM discusses situation → outputs [TASK] for Claude Code → Claude Code
clones repo, analyzes, fixes, pushes autonomously
Key changes:
- gather_context() auto-collects health, env, files each turn
- System prompt teaches agents they're "project managers", not coders
- parse_and_execute_turn() routes [TASK] blocks to action_claude_code()
- Removed: sub-agent delegation, emoji parser, 5-state machine, knowledge
dedup, ACT-phase guard, write dedup — all replaced by Claude Code
- Added: CONFIG_ERROR knowledge (HF env var / secret name collision)
- Kept: cooldown, building-state guard, chatlog, persistence, bilingual output
885 lines (was 1630, -45%)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/conversation-loop.py +337 -1081
|
@@ -1,72 +1,36 @@
|
|
| 1 |
#!/usr/bin/env python3 -u
|
| 2 |
"""
|
| 3 |
-
Adam & Eve —
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
- Read/write ANY file in the Dataset (memory, config, data...)
|
| 8 |
-
- Set environment variables and secrets
|
| 9 |
-
- Restart the Space
|
| 10 |
-
- Check health and logs
|
| 11 |
-
- Send messages to the child
|
| 12 |
-
|
| 13 |
-
The LLM decides what to do. Actions use [ACTION: ...] tags.
|
| 14 |
|
| 15 |
# ╔══════════════════════════════════════════════════════════════════════╗
|
| 16 |
-
# ║ SYSTEM ARCHITECTURE
|
| 17 |
# ╠══════════════════════════════════════════════════════════════════════╣
|
| 18 |
# ║ ║
|
| 19 |
-
# ║ ┌─────────────┐
|
| 20 |
-
# ║ │ Zhipu GLM │ ◄───────────
|
| 21 |
-
# ║ │ (glm-4.5) │
|
| 22 |
-
# ║ └─────────────┘
|
| 23 |
-
# ║
|
| 24 |
-
# ║
|
| 25 |
-
# ║
|
| 26 |
-
# ║
|
| 27 |
-
# ║
|
| 28 |
-
# ║ │
|
| 29 |
-
# ║ │
|
| 30 |
-
# ║ └──────
|
| 31 |
-
# ║ │ │ └────────────┘│ ║
|
| 32 |
-
# ║ ▼ │ ┌────────────┐│ ║
|
| 33 |
-
# ║ ┌─────────────┐ │ │ Knowledge ││ ║
|
| 34 |
-
# ║ │ HF ACTIONS │ │ │ Base ││ ║
|
| 35 |
-
# ║ │ create_child│ │ │ files_read ││ ║
|
| 36 |
-
# ║ │ check_health│ │ │ files_write││ ║
|
| 37 |
-
# ║ │ read/write │ │ │ errors_seen││ ║
|
| 38 |
-
# ║ │ set_env/sec │ │ └────────────┘│ ║
|
| 39 |
-
# ║ │ restart │ └────────────────┘ ║
|
| 40 |
-
# ║ │ send_bubble │ │ ║
|
| 41 |
-
# ║ └──────┬──────┘ │ ║
|
| 42 |
-
# ║ │ ▼ ║
|
| 43 |
-
# ║ ▼ ┌────────────────┐ ║
|
| 44 |
-
# ║ ┌─────────────┐ │ CHATLOG + │ ║
|
| 45 |
-
# ║ │ HuggingFace │ │ BUBBLE │ ║
|
| 46 |
-
# ║ │ Cain Space │ │ → Home Space │ ║
|
| 47 |
-
# ║ │ Cain Dataset│ │ → Adam/Eve │ ║
|
| 48 |
-
# ║ └─────────────┘ └────────────────┘ ║
|
| 49 |
-
# ║ ║
|
| 50 |
-
# ║ CAPABILITIES: ║
|
| 51 |
-
# ║ - Multi-action: up to 5 actions per turn (was 1) ║
|
| 52 |
-
# ║ - Sub-agent delegation: [ACTION: delegate:TASK] ║
|
| 53 |
-
# ║ - Parallel sub-tasks via ThreadPoolExecutor ║
|
| 54 |
-
# ║ ║
|
| 55 |
-
# ║ SAFETY LAYERS: ║
|
| 56 |
-
# ║ 1. Building-state guard: block write/restart during BUILDING ║
|
| 57 |
-
# ║ 2. Rebuild cooldown: 6-min dynamic cooldown after Space write ║
|
| 58 |
-
# ║ 3. ACT-phase guard: block reads when should be writing ║
|
| 59 |
-
# ║ 4. Knowledge dedup: block re-reading already-read files ║
|
| 60 |
-
# ║ 5. Config sanitizer: strip invalid openclaw.json keys ║
|
| 61 |
-
# ║ 6. Forced transitions: prevent infinite DIAGNOSE/VERIFY loops ║
|
| 62 |
-
# ║ 7. Shell-expression guard: block $(cmd) in set_env values ║
|
| 63 |
-
# ║ 8. Write dedup: block duplicate writes to same file per cycle ║
|
| 64 |
-
# ║ 9. Delegate depth limit: sub-agents cannot delegate further ║
|
| 65 |
# ║ ║
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
# ╚══════════════════════════════════════════════════════════════════════╝
|
| 67 |
"""
|
| 68 |
import json, time, re, requests, sys, os, io, subprocess
|
| 69 |
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 70 |
|
| 71 |
# Force unbuffered output
|
| 72 |
sys.stdout.reconfigure(line_buffering=True)
|
|
@@ -123,10 +87,7 @@ hf_api = HfApi(token=HF_TOKEN)
|
|
| 123 |
|
| 124 |
|
| 125 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 126 |
-
# MODULE 1: CHILD STATE
|
| 127 |
-
# Tracks Cain's current lifecycle: created? alive? stage? state?
|
| 128 |
-
# Updated by action_check_health(), action_restart(), etc.
|
| 129 |
-
# Used by state machine to decide transitions and by action parser for guards.
|
| 130 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 131 |
|
| 132 |
child_state = {
|
|
@@ -137,29 +98,24 @@ child_state = {
|
|
| 137 |
"detail": "",
|
| 138 |
}
|
| 139 |
|
| 140 |
-
#
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# Rebuild cooldown — prevent rapid write_file to Space that keeps resetting builds
|
| 145 |
-
REBUILD_COOLDOWN_SECS = 360 # 6 minutes (builds typically finish in 3-5 min)
|
| 146 |
-
last_rebuild_trigger_at = 0 # timestamp of last write_file to space
|
| 147 |
-
_pending_cooldown = False # defer cooldown activation until end of turn
|
| 148 |
-
files_written_this_cycle = set() # track files written since last RUNNING state
|
| 149 |
|
| 150 |
def check_and_clear_cooldown():
|
| 151 |
-
"""Auto-clear cooldown if Cain has finished building
|
| 152 |
global last_rebuild_trigger_at
|
| 153 |
if last_rebuild_trigger_at == 0:
|
| 154 |
return
|
| 155 |
elapsed = time.time() - last_rebuild_trigger_at
|
| 156 |
-
if elapsed < 60:
|
| 157 |
return
|
| 158 |
try:
|
| 159 |
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 160 |
stage = info.runtime.stage if info.runtime else "unknown"
|
| 161 |
if stage in ("RUNNING", "RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 162 |
-
print(f"[COOLDOWN] Build finished (stage={stage}), clearing cooldown
|
| 163 |
last_rebuild_trigger_at = 0
|
| 164 |
child_state["stage"] = stage
|
| 165 |
child_state["alive"] = (stage == "RUNNING")
|
|
@@ -186,22 +142,17 @@ def init_child_state():
|
|
| 186 |
except:
|
| 187 |
print(f"[init] {CHILD_NAME} does not exist yet")
|
| 188 |
|
| 189 |
-
|
| 190 |
init_child_state()
|
| 191 |
|
| 192 |
|
| 193 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 194 |
-
# MODULE 2: ACTIONS —
|
| 195 |
-
# Each action_*() function maps to one [ACTION: ...] tag the LLM can emit.
|
| 196 |
-
# Actions modify Cain's Space/Dataset via HuggingFace Hub API.
|
| 197 |
-
# Results are fed back to the LLM in the next turn's prompt.
|
| 198 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 199 |
|
| 200 |
def action_create_child():
|
| 201 |
"""Create Cain — a new HuggingFace Space."""
|
| 202 |
if child_state["created"]:
|
| 203 |
return f"{CHILD_NAME} already exists (stage: {child_state['stage']})."
|
| 204 |
-
|
| 205 |
print(f"[ACTION] Creating {CHILD_NAME}...")
|
| 206 |
try:
|
| 207 |
create_repo(CHILD_DATASET_ID, repo_type="dataset", token=HF_TOKEN,
|
|
@@ -220,28 +171,18 @@ def action_create_child():
|
|
| 220 |
token=HF_TOKEN, exist_ok=True, private=False, hardware="cpu-basic",
|
| 221 |
)
|
| 222 |
hf_api.add_space_secret(CHILD_SPACE_ID, "HF_TOKEN", HF_TOKEN)
|
| 223 |
-
# Add to Office
|
| 224 |
-
try:
|
| 225 |
-
current_vars = hf_api.get_space_variables("tao-shen/HuggingClaw-Office")
|
| 226 |
-
current_ra = current_vars.get("REMOTE_AGENTS", type("", (), {"value": ""})).value
|
| 227 |
-
if "cain|" not in current_ra:
|
| 228 |
-
new_ra = f"{current_ra},cain|{CHILD_NAME}|{CHILD_SPACE_URL}" if current_ra else f"cain|{CHILD_NAME}|{CHILD_SPACE_URL}"
|
| 229 |
-
hf_api.add_space_variable("tao-shen/HuggingClaw-Office", "REMOTE_AGENTS", new_ra)
|
| 230 |
-
except:
|
| 231 |
-
pass
|
| 232 |
child_state["created"] = True
|
| 233 |
child_state["stage"] = "BUILDING"
|
| 234 |
-
print(f"[ACTION]
|
| 235 |
-
return
|
| 236 |
-
f"Dataset: {CHILD_DATASET_ID}. Status: BUILDING. URL: {CHILD_SPACE_URL}")
|
| 237 |
except Exception as e:
|
| 238 |
return f"FAILED: {e}"
|
| 239 |
|
| 240 |
|
| 241 |
def action_check_health():
|
| 242 |
-
"""Check Cain's health."""
|
| 243 |
if not child_state["created"]:
|
| 244 |
-
return f"{CHILD_NAME} not born yet.
|
| 245 |
try:
|
| 246 |
resp = requests.get(f"{CHILD_SPACE_URL}/api/state", timeout=10)
|
| 247 |
if resp.ok:
|
|
@@ -250,9 +191,7 @@ def action_check_health():
|
|
| 250 |
child_state["state"] = data.get("state", "unknown")
|
| 251 |
child_state["detail"] = data.get("detail", "")
|
| 252 |
child_state["stage"] = "RUNNING"
|
| 253 |
-
|
| 254 |
-
return (f"{CHILD_NAME} is ALIVE! State: {child_state['state']}, "
|
| 255 |
-
f"Detail: {child_state['detail'] or 'healthy'}")
|
| 256 |
except:
|
| 257 |
pass
|
| 258 |
try:
|
|
@@ -261,17 +200,7 @@ def action_check_health():
|
|
| 261 |
child_state["stage"] = stage
|
| 262 |
child_state["alive"] = (stage == "RUNNING")
|
| 263 |
if stage in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR", "RUNNING"):
|
| 264 |
-
# Clear write dedup so agents can re-write files to fix issues
|
| 265 |
-
# RUNNING included: API may be unresponsive, agents need to patch code
|
| 266 |
-
# CONFIG_ERROR included: agents need to fix metadata/config issues
|
| 267 |
-
if files_written_this_cycle:
|
| 268 |
-
print(f"[DEDUP-CLEAR] {stage} detected — unlocking {len(files_written_this_cycle)} file(s) for re-write: {files_written_this_cycle}")
|
| 269 |
-
for f in files_written_this_cycle:
|
| 270 |
-
knowledge["files_read"].discard(f"space:{f}")
|
| 271 |
-
files_written_this_cycle.clear()
|
| 272 |
-
# Get error from runtime API + build logs for better diagnostics
|
| 273 |
error_detail = ""
|
| 274 |
-
build_log_snippet = ""
|
| 275 |
try:
|
| 276 |
rresp = requests.get(
|
| 277 |
f"https://huggingface.co/api/spaces/{CHILD_SPACE_ID}/runtime",
|
|
@@ -284,61 +213,9 @@ def action_check_health():
|
|
| 284 |
error_detail = " | ".join(lines[-5:])
|
| 285 |
except:
|
| 286 |
pass
|
| 287 |
-
|
| 288 |
-
try:
|
| 289 |
-
log_resp = requests.get(
|
| 290 |
-
f"https://api.hf.space/v1/{CHILD_SPACE_ID}/logs/run",
|
| 291 |
-
headers={"Authorization": f"Bearer {HF_TOKEN}"}, timeout=10,
|
| 292 |
-
stream=True)
|
| 293 |
-
if log_resp.ok:
|
| 294 |
-
log_lines = []
|
| 295 |
-
for line in log_resp.iter_lines(decode_unicode=True):
|
| 296 |
-
if line and line.startswith("data:"):
|
| 297 |
-
try:
|
| 298 |
-
entry = json.loads(line[5:])
|
| 299 |
-
log_lines.append(entry.get("data", "").strip())
|
| 300 |
-
except:
|
| 301 |
-
pass
|
| 302 |
-
if len(log_lines) >= 30:
|
| 303 |
-
break
|
| 304 |
-
# Get last meaningful log lines (skip empty, focus on errors)
|
| 305 |
-
meaningful = [l for l in log_lines if l and len(l) > 5]
|
| 306 |
-
if meaningful:
|
| 307 |
-
build_log_snippet = "\nRECENT LOGS:\n" + "\n".join(meaningful[-10:])
|
| 308 |
-
except:
|
| 309 |
-
pass
|
| 310 |
-
return (f"{CHILD_NAME} has a {stage}! "
|
| 311 |
-
f"Error: {error_detail or 'unknown'}. "
|
| 312 |
-
f"{build_log_snippet}"
|
| 313 |
-
f"\nOptions: [ACTION: restart] or fix code with [ACTION: write_file:space:PATH] "
|
| 314 |
-
f"or config with [ACTION: write_file:dataset:.openclaw/openclaw.json]")
|
| 315 |
if stage in ("BUILDING", "STARTING", "APP_STARTING"):
|
| 316 |
return f"{CHILD_NAME} is starting up (stage: {stage}). Be patient."
|
| 317 |
-
if stage == "RUNNING":
|
| 318 |
-
# API not responding — fetch runtime logs to help agents diagnose
|
| 319 |
-
log_snippet = ""
|
| 320 |
-
try:
|
| 321 |
-
log_resp = requests.get(
|
| 322 |
-
f"https://api.hf.space/v1/{CHILD_SPACE_ID}/logs/run",
|
| 323 |
-
headers={"Authorization": f"Bearer {HF_TOKEN}"}, timeout=10,
|
| 324 |
-
stream=True)
|
| 325 |
-
if log_resp.ok:
|
| 326 |
-
log_lines = []
|
| 327 |
-
for line in log_resp.iter_lines(decode_unicode=True):
|
| 328 |
-
if line and line.startswith("data:"):
|
| 329 |
-
try:
|
| 330 |
-
entry = json.loads(line[5:])
|
| 331 |
-
log_lines.append(entry.get("data", "").strip())
|
| 332 |
-
except:
|
| 333 |
-
pass
|
| 334 |
-
if len(log_lines) >= 30:
|
| 335 |
-
break
|
| 336 |
-
meaningful = [l for l in log_lines if l and len(l) > 5]
|
| 337 |
-
if meaningful:
|
| 338 |
-
log_snippet = "\nRUNTIME LOGS (last 10 lines):\n" + "\n".join(meaningful[-10:])
|
| 339 |
-
except:
|
| 340 |
-
pass
|
| 341 |
-
return f"{CHILD_NAME} stage: RUNNING. Running but API not responding.{log_snippet}"
|
| 342 |
return f"{CHILD_NAME} stage: {stage}."
|
| 343 |
except Exception as e:
|
| 344 |
return f"Cannot reach {CHILD_NAME}: {e}"
|
|
@@ -353,122 +230,12 @@ def action_restart():
|
|
| 353 |
hf_api.restart_space(CHILD_SPACE_ID)
|
| 354 |
child_state["alive"] = False
|
| 355 |
child_state["stage"] = "RESTARTING"
|
| 356 |
-
_pending_cooldown = True
|
| 357 |
-
return f"{CHILD_NAME} is restarting.
|
| 358 |
except Exception as e:
|
| 359 |
return f"Restart failed: {e}"
|
| 360 |
|
| 361 |
|
| 362 |
-
def action_list_files(target):
|
| 363 |
-
"""List files in the child's Space repo or Dataset."""
|
| 364 |
-
repo_type = "space" if target == "space" else "dataset"
|
| 365 |
-
repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
|
| 366 |
-
try:
|
| 367 |
-
files = hf_api.list_repo_files(repo_id, repo_type=repo_type)
|
| 368 |
-
return f"Files in {CHILD_NAME}'s {target} ({repo_id}):\n" + "\n".join(f" {f}" for f in files)
|
| 369 |
-
except Exception as e:
|
| 370 |
-
return f"Error listing files: {e}"
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
def action_read_file(target, path):
|
| 374 |
-
"""Read a file from the child's Space or Dataset."""
|
| 375 |
-
repo_type = "space" if target == "space" else "dataset"
|
| 376 |
-
repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
|
| 377 |
-
try:
|
| 378 |
-
local = hf_hub_download(repo_id, path, repo_type=repo_type, token=HF_TOKEN,
|
| 379 |
-
force_download=True)
|
| 380 |
-
with open(local, errors='replace') as f:
|
| 381 |
-
content = f.read()
|
| 382 |
-
if len(content) > 4000:
|
| 383 |
-
content = content[:4000] + f"\n... (truncated, total {len(content)} chars)"
|
| 384 |
-
return f"=== {target}:{path} ===\n{content}"
|
| 385 |
-
except Exception as e:
|
| 386 |
-
return f"Error reading {target}:{path}: {e}"
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
def action_write_file(target, path, content):
|
| 390 |
-
"""Write a file to the child's Space or Dataset."""
|
| 391 |
-
repo_type = "space" if target == "space" else "dataset"
|
| 392 |
-
repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
|
| 393 |
-
|
| 394 |
-
# Safety: validate openclaw.json before writing
|
| 395 |
-
if path.endswith("openclaw.json"):
|
| 396 |
-
try:
|
| 397 |
-
cfg = json.loads(content)
|
| 398 |
-
# Remove keys known to cause RUNTIME_ERROR in OpenClaw
|
| 399 |
-
invalid_keys = ["agent", "auth.defaultScope", "gateway.auth.scope"]
|
| 400 |
-
removed = []
|
| 401 |
-
for k in invalid_keys:
|
| 402 |
-
if k in cfg:
|
| 403 |
-
del cfg[k]
|
| 404 |
-
removed.append(k)
|
| 405 |
-
if "models" in cfg and "defaultModel" in cfg["models"]:
|
| 406 |
-
del cfg["models"]["defaultModel"]
|
| 407 |
-
removed.append("models.defaultModel")
|
| 408 |
-
if removed:
|
| 409 |
-
content = json.dumps(cfg, indent=2)
|
| 410 |
-
print(f"[SAFETY] Removed invalid config keys: {removed}")
|
| 411 |
-
except json.JSONDecodeError:
|
| 412 |
-
return f"Error: invalid JSON in config file. Please fix the content."
|
| 413 |
-
|
| 414 |
-
try:
|
| 415 |
-
global _pending_cooldown
|
| 416 |
-
hf_api.upload_file(
|
| 417 |
-
path_or_fileobj=io.BytesIO(content.encode()),
|
| 418 |
-
path_in_repo=path,
|
| 419 |
-
repo_id=repo_id, repo_type=repo_type,
|
| 420 |
-
)
|
| 421 |
-
rebuild_note = ""
|
| 422 |
-
if target == "space":
|
| 423 |
-
_pending_cooldown = True # deferred — activated after turn ends
|
| 424 |
-
rebuild_note = " ⚠️ This triggers a Space rebuild! Cooldown starts after this turn."
|
| 425 |
-
return f"✓ Wrote {len(content)} bytes to {CHILD_NAME}'s {target}:{path}{rebuild_note}"
|
| 426 |
-
except Exception as e:
|
| 427 |
-
return f"Error writing {target}:{path}: {e}"
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
def action_delete_file(target, path):
|
| 431 |
-
"""Delete a file from the child's Space or Dataset."""
|
| 432 |
-
repo_type = "space" if target == "space" else "dataset"
|
| 433 |
-
repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
|
| 434 |
-
try:
|
| 435 |
-
global _pending_cooldown
|
| 436 |
-
hf_api.delete_file(
|
| 437 |
-
path_in_repo=path,
|
| 438 |
-
repo_id=repo_id, repo_type=repo_type,
|
| 439 |
-
)
|
| 440 |
-
rebuild_note = ""
|
| 441 |
-
if target == "space":
|
| 442 |
-
_pending_cooldown = True # deferred — activated after turn ends
|
| 443 |
-
rebuild_note = " ⚠️ This triggers a Space rebuild! Cooldown starts after this turn."
|
| 444 |
-
return f"✓ Deleted {target}:{path}{rebuild_note}"
|
| 445 |
-
except Exception as e:
|
| 446 |
-
return f"Error deleting {target}:{path}: {e}"
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
def action_set_env(key, value):
|
| 450 |
-
"""Set an environment variable on the child's Space."""
|
| 451 |
-
# Block shell expressions — LLM sometimes writes $(cmd) or backticks as values
|
| 452 |
-
if '$(' in value or '`' in value or value.startswith('$('):
|
| 453 |
-
return (f"⛔ BLOCKED: Value contains shell expression which won't be evaluated. "
|
| 454 |
-
f"Provide the actual value, not a shell command. "
|
| 455 |
-
f"HF_TOKEN is already set as a secret — use [ACTION: get_env] to check.")
|
| 456 |
-
try:
|
| 457 |
-
hf_api.add_space_variable(CHILD_SPACE_ID, key, value)
|
| 458 |
-
return f"✓ Set env var {key}={value} on {CHILD_NAME}'s Space"
|
| 459 |
-
except Exception as e:
|
| 460 |
-
return f"Error: {e}"
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
def action_set_secret(key, value):
|
| 464 |
-
"""Set a secret on the child's Space."""
|
| 465 |
-
try:
|
| 466 |
-
hf_api.add_space_secret(CHILD_SPACE_ID, key, value)
|
| 467 |
-
return f"✓ Set secret {key} on {CHILD_NAME}'s Space (value hidden)"
|
| 468 |
-
except Exception as e:
|
| 469 |
-
return f"Error: {e}"
|
| 470 |
-
|
| 471 |
-
|
| 472 |
def action_get_env():
|
| 473 |
"""List environment variables and secrets on the child's Space."""
|
| 474 |
try:
|
|
@@ -478,7 +245,6 @@ def action_get_env():
|
|
| 478 |
lines.append(" Variables:")
|
| 479 |
for k, v in vars_dict.items():
|
| 480 |
lines.append(f" {k} = {v.value}")
|
| 481 |
-
# Also check secrets (names only, values hidden)
|
| 482 |
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 483 |
if hasattr(info, 'runtime') and info.runtime and hasattr(info.runtime, 'secrets'):
|
| 484 |
secrets = info.runtime.secrets
|
|
@@ -486,24 +252,33 @@ def action_get_env():
|
|
| 486 |
lines.append(" Secrets (values hidden):")
|
| 487 |
for s in secrets:
|
| 488 |
lines.append(f" {s} = ****")
|
| 489 |
-
if len(lines) == 1:
|
| 490 |
-
return f"{CHILD_NAME} has no environment variables or secrets set."
|
| 491 |
return "\n".join(lines)
|
| 492 |
except Exception as e:
|
| 493 |
return f"Error: {e}"
|
| 494 |
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
def action_send_bubble(text):
|
| 497 |
-
"""Send a message to the child
|
| 498 |
try:
|
| 499 |
requests.post(f"{CHILD_SPACE_URL}/api/bubble",
|
| 500 |
json={"text": text, "text_zh": text}, timeout=5)
|
| 501 |
-
return f"
|
| 502 |
except Exception as e:
|
| 503 |
-
return f"Error
|
| 504 |
|
| 505 |
|
| 506 |
-
# ── Claude Code Action ─────────────────────────────────────────────
|
| 507 |
|
| 508 |
CLAUDE_WORK_DIR = "/tmp/claude-workspace"
|
| 509 |
CLAUDE_TIMEOUT = 300 # 5 minutes
|
|
@@ -511,7 +286,7 @@ CLAUDE_TIMEOUT = 300 # 5 minutes
|
|
| 511 |
def action_claude_code(task):
|
| 512 |
"""Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
|
| 513 |
if not child_state["created"]:
|
| 514 |
-
return f"{CHILD_NAME} not born yet.
|
| 515 |
|
| 516 |
global _pending_cooldown
|
| 517 |
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{CHILD_SPACE_ID}"
|
|
@@ -553,10 +328,10 @@ def action_claude_code(task):
|
|
| 553 |
"ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4.7",
|
| 554 |
"ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.7",
|
| 555 |
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.5-Air",
|
| 556 |
-
"CI": "true",
|
| 557 |
})
|
| 558 |
|
| 559 |
-
print(f"[CLAUDE-CODE] Running: {task[:
|
| 560 |
try:
|
| 561 |
result = subprocess.run(
|
| 562 |
["claude", "-p", task, "--output-format", "text"],
|
|
@@ -574,7 +349,7 @@ def action_claude_code(task):
|
|
| 574 |
except FileNotFoundError:
|
| 575 |
return "Claude Code CLI not found. Is @anthropic-ai/claude-code installed?"
|
| 576 |
except Exception as e:
|
| 577 |
-
return f"Claude Code failed
|
| 578 |
|
| 579 |
# 3. Push changes back to Cain's Space
|
| 580 |
try:
|
|
@@ -594,388 +369,74 @@ def action_claude_code(task):
|
|
| 594 |
subprocess.run("git push", shell=True, cwd=CLAUDE_WORK_DIR,
|
| 595 |
timeout=60, capture_output=True, check=True)
|
| 596 |
push_result = f"Pushed changes:\n{status_out}"
|
| 597 |
-
_pending_cooldown = True
|
| 598 |
print(f"[CLAUDE-CODE] Pushed: {status_out}")
|
| 599 |
except Exception as e:
|
| 600 |
push_result = f"Push failed: {e}"
|
| 601 |
|
| 602 |
-
# Truncate output to fit LLM context
|
| 603 |
if len(output) > 3000:
|
| 604 |
-
output = output[:3000] + f"\n... (truncated,
|
| 605 |
|
| 606 |
return f"=== Claude Code Output ===\n{output}\n\n=== Changes ===\n{push_result}"
|
| 607 |
|
| 608 |
|
| 609 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 610 |
-
# MODULE
|
| 611 |
-
# execute_subtask(): Spawns a focused sub-agent with its own LLM call.
|
| 612 |
-
# Used by [ACTION: delegate:TASK] — enables parallel sub-agent work.
|
| 613 |
-
# Sub-agents share the same action set but cannot delegate further (depth=1).
|
| 614 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 615 |
|
| 616 |
-
|
| 617 |
-
"""Execute a focused sub-task with its own LLM call and actions."""
|
| 618 |
-
status = get_child_status() if 'get_child_status' in dir() else f"stage={child_state['stage']}"
|
| 619 |
-
|
| 620 |
-
sub_system = f"""You are a focused sub-agent working for {parent_speaker}.
|
| 621 |
-
Your single task: {task_description}
|
| 622 |
-
|
| 623 |
-
You have access to {CHILD_NAME}'s Space and Dataset:
|
| 624 |
-
[ACTION: check_health]
|
| 625 |
-
[ACTION: list_files:space] / [ACTION: list_files:dataset]
|
| 626 |
-
[ACTION: read_file:space:PATH] / [ACTION: read_file:dataset:PATH]
|
| 627 |
-
[ACTION: write_file:space:PATH] with [CONTENT]...[/CONTENT]
|
| 628 |
-
[ACTION: write_file:dataset:PATH] with [CONTENT]...[/CONTENT]
|
| 629 |
-
[ACTION: set_env:KEY:VALUE] / [ACTION: set_secret:KEY:VALUE]
|
| 630 |
-
[ACTION: restart] / [ACTION: get_env]
|
| 631 |
-
[ACTION: claude_code:TASK] — Run Claude Code for complex coding fixes
|
| 632 |
-
|
| 633 |
-
CHILD STATUS: {status}
|
| 634 |
-
|
| 635 |
-
RULES:
|
| 636 |
-
1. Be concise — report findings in 2-3 sentences
|
| 637 |
-
2. Execute 1-3 actions to complete your task
|
| 638 |
-
3. No delegation — you cannot create sub-agents
|
| 639 |
-
4. Focus ONLY on your assigned task
|
| 640 |
-
5. For complex code changes, prefer [ACTION: claude_code:TASK] over manual write_file"""
|
| 641 |
-
|
| 642 |
-
sub_user = f"Execute this task now: {task_description}"
|
| 643 |
-
|
| 644 |
-
print(f"[SUB-AGENT] Starting: {task_description[:80]}")
|
| 645 |
-
reply = call_llm(sub_system, sub_user)
|
| 646 |
-
if not reply:
|
| 647 |
-
print(f"[SUB-AGENT] No response for: {task_description[:60]}")
|
| 648 |
-
return {"task": task_description, "result": "(sub-agent: no response)", "actions": []}
|
| 649 |
-
|
| 650 |
-
clean, actions = parse_and_execute_actions(reply, depth=1)
|
| 651 |
-
|
| 652 |
-
summary_parts = [f"Sub-agent result for '{task_description}':"]
|
| 653 |
-
if clean:
|
| 654 |
-
summary_parts.append(f" Finding: {clean[:400]}")
|
| 655 |
-
for ar in actions:
|
| 656 |
-
summary_parts.append(f" Action: {ar['action']} → {ar['result'][:200]}")
|
| 657 |
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
|
|
|
|
|
|
|
| 662 |
|
| 663 |
-
#
|
| 664 |
-
|
| 665 |
-
# Parse order: 1) [ACTION: write_file] with [CONTENT] block
|
| 666 |
-
# 2) [ACTION/Action/操作/动作: ...] tags (case-insensitive, one per turn)
|
| 667 |
-
# 3) 🔧🛠️ emoji format fallback (LLM sometimes uses this)
|
| 668 |
-
# Safety guards applied: building-state, ACT-phase, knowledge dedup, shell-expr.
|
| 669 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 670 |
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
# and [/CONTENT] with whitespace/newline before closing bracket
|
| 683 |
-
write_match = re.search(
|
| 684 |
-
r'\[(?:(?:ACTION|Action|action|操作|动作)\s*[::]\s*)?write_file\s*:\s*(\w+)\s*:\s*([^\]]+)\]\s*\[CONTENT\](.*?)\[/\s*CONTENT\s*\]',
|
| 685 |
-
raw_text, re.DOTALL
|
| 686 |
-
)
|
| 687 |
-
if write_match:
|
| 688 |
-
target, path, content = write_match.group(1), write_match.group(2).strip(), write_match.group(3).strip()
|
| 689 |
-
key = f"write_file:{target}:{path}"
|
| 690 |
-
file_id = f"{target}:{path}"
|
| 691 |
-
if key not in executed:
|
| 692 |
-
executed.add(key)
|
| 693 |
-
# Guard: duplicate write to same file this cycle
|
| 694 |
-
if target == "space" and file_id in files_written_this_cycle:
|
| 695 |
-
result = (f"⛔ BLOCKED: {path} was already written this cycle. "
|
| 696 |
-
"Wait for the build to finish and verify before writing again. "
|
| 697 |
-
"Writing the same file twice wastes a rebuild cycle.")
|
| 698 |
-
results.append({"action": key, "result": result})
|
| 699 |
-
print(f"[BLOCKED] {key} — duplicate write this cycle")
|
| 700 |
-
# Guard: block write_file during BUILDING/RESTARTING (would reset build)
|
| 701 |
-
# APP_STARTING is allowed — writing triggers a new build which may fix the stuck state
|
| 702 |
-
elif target == "space" and child_state["stage"] in ("BUILDING", "RESTARTING"):
|
| 703 |
-
result = (f"⛔ BLOCKED: Cain is currently {child_state['stage']}. "
|
| 704 |
-
"Writing to Space during build RESETS the entire build from scratch. "
|
| 705 |
-
"Wait for it to finish, then try again.")
|
| 706 |
-
results.append({"action": key, "result": result})
|
| 707 |
-
print(f"[BLOCKED] {key} — Cain is {child_state['stage']}")
|
| 708 |
-
# Guard: rebuild cooldown (check dynamically first)
|
| 709 |
-
elif target == "space" and last_rebuild_trigger_at > 0:
|
| 710 |
-
check_and_clear_cooldown() # may clear cooldown early if build done
|
| 711 |
-
elapsed = time.time() - last_rebuild_trigger_at if last_rebuild_trigger_at > 0 else 9999
|
| 712 |
-
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 713 |
-
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 714 |
-
result = (f"⛔ BLOCKED: Rebuild cooldown active ({remaining}s remaining). "
|
| 715 |
-
"Every write_file to Space triggers a full rebuild.")
|
| 716 |
-
results.append({"action": key, "result": result})
|
| 717 |
-
print(f"[BLOCKED] {key} — rebuild cooldown ({remaining}s remaining)")
|
| 718 |
-
else:
|
| 719 |
-
result = action_write_file(target, path, content)
|
| 720 |
-
results.append({"action": key, "result": result})
|
| 721 |
-
print(f"[ACTION] {key} → {result[:100]}")
|
| 722 |
-
files_written_this_cycle.add(file_id)
|
| 723 |
-
# Clear knowledge cache so agents can re-read the file they just wrote
|
| 724 |
-
knowledge["files_read"].discard(file_id)
|
| 725 |
-
else:
|
| 726 |
-
result = action_write_file(target, path, content)
|
| 727 |
-
results.append({"action": key, "result": result})
|
| 728 |
-
print(f"[ACTION] {key} → {result[:100]}")
|
| 729 |
-
if target == "space":
|
| 730 |
-
files_written_this_cycle.add(file_id)
|
| 731 |
-
knowledge["files_read"].discard(file_id)
|
| 732 |
-
|
| 733 |
-
# 2. Handle all [ACTION/Action/操作/动作: ...] tags — case-insensitive, multilingual
|
| 734 |
-
for match in re.finditer(r'\[(?:ACTION|Action|action|操作|动作)\s*[::]\s*([^\]]+)\]', raw_text):
|
| 735 |
-
action_str = match.group(1).strip()
|
| 736 |
-
|
| 737 |
-
# Skip write_file (handled above)
|
| 738 |
-
if action_str.startswith("write_file"):
|
| 739 |
-
continue
|
| 740 |
|
| 741 |
-
|
| 742 |
-
if action_str in executed:
|
| 743 |
-
continue
|
| 744 |
-
executed.add(action_str)
|
| 745 |
-
|
| 746 |
-
# Parse action name and arguments (colon-separated)
|
| 747 |
-
parts = [p.strip() for p in action_str.split(":")]
|
| 748 |
-
name = parts[0]
|
| 749 |
-
args = parts[1:]
|
| 750 |
-
|
| 751 |
-
# Cap at MAX_ACTIONS_PER_TURN (multi-action support)
|
| 752 |
-
if len(results) >= MAX_ACTIONS_PER_TURN:
|
| 753 |
-
break
|
| 754 |
-
|
| 755 |
-
# Block restart/write when Cain is building/restarting — would reset build
|
| 756 |
-
# APP_STARTING is allowed so agents can fix stuck startups
|
| 757 |
-
if child_state["stage"] in ("BUILDING", "RESTARTING") and name in ("restart", "write_file", "set_env", "set_secret", "claude_code"):
|
| 758 |
-
result = (f"⛔ BLOCKED: Cain is currently {child_state['stage']}. "
|
| 759 |
-
"Do NOT restart or make changes — wait for it to finish. "
|
| 760 |
-
"Every write_file during build RESETS the entire build from scratch. "
|
| 761 |
-
"Use [ACTION: check_health] to monitor progress.")
|
| 762 |
-
results.append({"action": action_str, "result": result})
|
| 763 |
-
print(f"[BLOCKED] {name} — Cain is {child_state['stage']}")
|
| 764 |
-
break
|
| 765 |
-
|
| 766 |
-
# Rebuild cooldown — prevent writing to Space repo too soon after last rebuild trigger
|
| 767 |
-
if name in ("write_file", "set_env", "set_secret", "restart", "delete_file", "claude_code") and last_rebuild_trigger_at > 0:
|
| 768 |
-
check_and_clear_cooldown() # may clear cooldown early if build done
|
| 769 |
-
elapsed = time.time() - last_rebuild_trigger_at if last_rebuild_trigger_at > 0 else 9999
|
| 770 |
-
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 771 |
-
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 772 |
-
result = (f"⛔ BLOCKED: Rebuild cooldown active — last Space change was {int(elapsed)}s ago. "
|
| 773 |
-
f"Wait {remaining}s more before making changes. "
|
| 774 |
-
"Every write_file to Space triggers a full rebuild, resetting progress. "
|
| 775 |
-
"Use [ACTION: check_health] to monitor the current build.")
|
| 776 |
-
results.append({"action": action_str, "result": result})
|
| 777 |
-
print(f"[BLOCKED] {name} — rebuild cooldown ({remaining}s remaining)")
|
| 778 |
-
continue # Don't kill remaining actions — reads/checks can still proceed
|
| 779 |
-
|
| 780 |
-
# Block read-only actions based on workflow state
|
| 781 |
-
if workflow_state == "ACT" and name in ("read_file", "list_files", "check_health"):
|
| 782 |
-
result = (f"⛔ BLOCKED: You are in ACTION phase. "
|
| 783 |
-
"You MUST use write_file, set_env, set_secret, or restart. "
|
| 784 |
-
"You already have enough information — make a change NOW.")
|
| 785 |
-
results.append({"action": action_str, "result": result})
|
| 786 |
-
print(f"[BLOCKED] {name} — forced ACT phase")
|
| 787 |
-
continue # Don't kill remaining actions — writes after a blocked read should still execute
|
| 788 |
-
|
| 789 |
-
# Block re-reading files already in knowledge base
|
| 790 |
-
if name == "read_file" and len(args) >= 2:
|
| 791 |
-
file_key = ":".join(args)
|
| 792 |
-
if file_key in knowledge["files_read"]:
|
| 793 |
-
result = (f"⛔ You already read {file_key}. Use the information you have. "
|
| 794 |
-
"If you need to change it, use [ACTION: write_file:...]. "
|
| 795 |
-
"If you need a different file, read a NEW one.")
|
| 796 |
-
results.append({"action": action_str, "result": result})
|
| 797 |
-
print(f"[BLOCKED] {name} — already read {file_key}")
|
| 798 |
-
continue # Don't kill remaining actions — skip this read, execute the rest
|
| 799 |
-
|
| 800 |
-
result = None
|
| 801 |
-
if name == "create_child":
|
| 802 |
-
result = action_create_child()
|
| 803 |
-
elif name == "check_health":
|
| 804 |
-
result = action_check_health()
|
| 805 |
-
elif name == "restart":
|
| 806 |
-
result = action_restart()
|
| 807 |
-
elif name == "list_files" and len(args) >= 1:
|
| 808 |
-
result = action_list_files(args[0])
|
| 809 |
-
elif name == "read_file" and len(args) >= 2:
|
| 810 |
-
result = action_read_file(args[0], ":".join(args[1:])) # path may have colons
|
| 811 |
-
elif name == "set_env" and len(args) >= 2:
|
| 812 |
-
result = action_set_env(args[0], ":".join(args[1:]))
|
| 813 |
-
elif name == "set_secret" and len(args) >= 2:
|
| 814 |
-
result = action_set_secret(args[0], ":".join(args[1:]))
|
| 815 |
-
elif name == "delete_file" and len(args) >= 2:
|
| 816 |
-
result = action_delete_file(args[0], ":".join(args[1:]))
|
| 817 |
-
elif name == "get_env":
|
| 818 |
-
result = action_get_env()
|
| 819 |
-
elif name == "send_bubble" and len(args) >= 1:
|
| 820 |
-
result = action_send_bubble(":".join(args)) # rejoin in case message has colons
|
| 821 |
-
elif name == "claude_code" and len(args) >= 1:
|
| 822 |
-
task_desc = ":".join(args)
|
| 823 |
-
result = action_claude_code(task_desc)
|
| 824 |
-
elif name == "delegate" and len(args) >= 1:
|
| 825 |
-
task_desc = ":".join(args)
|
| 826 |
-
if depth >= MAX_DELEGATE_DEPTH:
|
| 827 |
-
result = "⛔ Sub-agents cannot delegate further. Execute the task directly."
|
| 828 |
-
else:
|
| 829 |
-
# Defer delegate execution for parallel batch later
|
| 830 |
-
pending_delegates.append({"action_str": action_str, "task": task_desc})
|
| 831 |
-
result = None # Will be filled after parallel execution
|
| 832 |
-
else:
|
| 833 |
-
result = f"Unknown action: {action_str}"
|
| 834 |
|
| 835 |
-
if result:
|
| 836 |
-
results.append({"action": action_str, "result": result})
|
| 837 |
-
print(f"[ACTION] {action_str} → {result[:120]}")
|
| 838 |
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
args = parts[1:]
|
| 850 |
-
|
| 851 |
-
if len(results) >= MAX_ACTIONS_PER_TURN:
|
| 852 |
-
break
|
| 853 |
-
|
| 854 |
-
# Apply same blocking rules
|
| 855 |
-
if child_state["stage"] in ("BUILDING", "RESTARTING") and name in ("restart", "write_file", "set_env", "set_secret", "claude_code"):
|
| 856 |
-
result = (f"⛔ BLOCKED: Cain is currently {child_state['stage']}. Wait for it to finish. Writing during build RESETS it.")
|
| 857 |
-
results.append({"action": action_str, "result": result})
|
| 858 |
-
print(f"[BLOCKED] sub-agent {name} — Cain is {child_state['stage']}")
|
| 859 |
-
break
|
| 860 |
-
|
| 861 |
-
# Rebuild cooldown (emoji parser)
|
| 862 |
-
if name in ("write_file", "set_env", "set_secret", "restart", "delete_file") and last_rebuild_trigger_at > 0:
|
| 863 |
-
elapsed = time.time() - last_rebuild_trigger_at
|
| 864 |
-
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 865 |
-
remaining = int(REBUILD_COOLDOWN_SECS - elapsed)
|
| 866 |
-
result = (f"⛔ BLOCKED: Rebuild cooldown — wait {remaining}s more. "
|
| 867 |
-
"Use [ACTION: check_health] to monitor.")
|
| 868 |
-
results.append({"action": action_str, "result": result})
|
| 869 |
-
print(f"[BLOCKED-emoji] {name} — rebuild cooldown ({remaining}s remaining)")
|
| 870 |
-
break
|
| 871 |
-
|
| 872 |
-
if workflow_state == "ACT" and name in ("read_file", "list_files", "check_health"):
|
| 873 |
-
result = (f"⛔ BLOCKED: You are in ACTION phase. "
|
| 874 |
-
"You MUST use write_file, set_env, set_secret, or restart.")
|
| 875 |
-
results.append({"action": action_str, "result": result})
|
| 876 |
-
print(f"[BLOCKED-emoji] {name} — forced ACT phase")
|
| 877 |
-
break
|
| 878 |
-
|
| 879 |
-
if name == "read_file" and len(args) >= 2:
|
| 880 |
-
file_key = ":".join(args)
|
| 881 |
-
if file_key in knowledge["files_read"]:
|
| 882 |
-
result = (f"⛔ You already read {file_key}. Use the information you have.")
|
| 883 |
-
results.append({"action": action_str, "result": result})
|
| 884 |
-
print(f"[BLOCKED-emoji] {name} — already read {file_key}")
|
| 885 |
-
break
|
| 886 |
-
|
| 887 |
-
result = None
|
| 888 |
-
if name == "create_child":
|
| 889 |
-
result = action_create_child()
|
| 890 |
-
elif name == "check_health":
|
| 891 |
-
result = action_check_health()
|
| 892 |
-
elif name == "restart":
|
| 893 |
-
result = action_restart()
|
| 894 |
-
elif name == "list_files" and len(args) >= 1:
|
| 895 |
-
result = action_list_files(args[0])
|
| 896 |
-
elif name == "read_file" and len(args) >= 2:
|
| 897 |
-
result = action_read_file(args[0], ":".join(args[1:]))
|
| 898 |
-
elif name == "set_env" and len(args) >= 2:
|
| 899 |
-
result = action_set_env(args[0], ":".join(args[1:]))
|
| 900 |
-
elif name == "set_secret" and len(args) >= 2:
|
| 901 |
-
result = action_set_secret(args[0], ":".join(args[1:]))
|
| 902 |
-
elif name == "delete_file" and len(args) >= 2:
|
| 903 |
-
result = action_delete_file(args[0], ":".join(args[1:]))
|
| 904 |
-
elif name == "get_env":
|
| 905 |
-
result = action_get_env()
|
| 906 |
-
elif name == "send_bubble" and len(args) >= 1:
|
| 907 |
-
result = action_send_bubble(":".join(args))
|
| 908 |
-
elif name == "claude_code" and len(args) >= 1:
|
| 909 |
-
task_desc = ":".join(args)
|
| 910 |
-
result = action_claude_code(task_desc)
|
| 911 |
-
elif name == "delegate" and len(args) >= 1:
|
| 912 |
-
task_desc = ":".join(args)
|
| 913 |
-
if depth >= MAX_DELEGATE_DEPTH:
|
| 914 |
-
result = "⛔ Sub-agents cannot delegate further."
|
| 915 |
-
else:
|
| 916 |
-
pending_delegates.append({"action_str": action_str, "task": task_desc})
|
| 917 |
-
result = None
|
| 918 |
-
|
| 919 |
-
if result:
|
| 920 |
-
results.append({"action": action_str, "result": result})
|
| 921 |
-
print(f"[ACTION-emoji] {action_str} → {result[:120]}")
|
| 922 |
-
|
| 923 |
-
# 4. Execute pending delegate tasks in parallel
|
| 924 |
-
if pending_delegates:
|
| 925 |
-
if len(pending_delegates) == 1:
|
| 926 |
-
# Single delegate — run directly
|
| 927 |
-
d = pending_delegates[0]
|
| 928 |
-
print(f"[DELEGATE] Running 1 sub-agent: {d['task'][:60]}")
|
| 929 |
-
subtask = execute_subtask(d["task"], "agent")
|
| 930 |
-
results.append({"action": d["action_str"], "result": subtask["result"]})
|
| 931 |
-
for sa in subtask["actions"]:
|
| 932 |
-
action_history.append({"turn": turn_count, "speaker": "sub-agent",
|
| 933 |
-
"action": sa["action"], "result": sa["result"][:200]})
|
| 934 |
-
else:
|
| 935 |
-
# Multiple delegates — run in parallel!
|
| 936 |
-
print(f"[DELEGATE] Running {len(pending_delegates)} sub-agents in PARALLEL")
|
| 937 |
-
with ThreadPoolExecutor(max_workers=min(3, len(pending_delegates))) as pool:
|
| 938 |
-
future_to_delegate = {
|
| 939 |
-
pool.submit(execute_subtask, d["task"], "agent"): d
|
| 940 |
-
for d in pending_delegates
|
| 941 |
-
}
|
| 942 |
-
for future in as_completed(future_to_delegate):
|
| 943 |
-
d = future_to_delegate[future]
|
| 944 |
-
try:
|
| 945 |
-
subtask = future.result(timeout=120)
|
| 946 |
-
results.append({"action": d["action_str"], "result": subtask["result"]})
|
| 947 |
-
for sa in subtask["actions"]:
|
| 948 |
-
action_history.append({"turn": turn_count, "speaker": "sub-agent",
|
| 949 |
-
"action": sa["action"], "result": sa["result"][:200]})
|
| 950 |
-
print(f"[DELEGATE] ✓ Done: {d['task'][:60]}")
|
| 951 |
-
except Exception as e:
|
| 952 |
-
results.append({"action": d["action_str"],
|
| 953 |
-
"result": f"Sub-agent failed: {e}"})
|
| 954 |
-
print(f"[DELEGATE] ✗ Failed: {d['task'][:60]} — {e}")
|
| 955 |
-
|
| 956 |
-
# 5. Activate deferred cooldown AFTER all actions in this turn complete
|
| 957 |
-
# This allows agents to batch multiple file ops (e.g., write app.py + requirements.txt)
|
| 958 |
-
# in a single turn without the first write blocking the second.
|
| 959 |
-
if _pending_cooldown and depth == 0: # only at top-level, not inside sub-agents
|
| 960 |
-
last_rebuild_trigger_at = time.time()
|
| 961 |
-
_pending_cooldown = False
|
| 962 |
-
print(f"[COOLDOWN] Activated — Space was modified this turn. Next write blocked for {REBUILD_COOLDOWN_SECS}s (or until build finishes).")
|
| 963 |
|
| 964 |
-
# Clean the text: remove action tags, content blocks, and emoji actions
|
| 965 |
-
clean = re.sub(r'\[(?:ACTION|Action|action|操作|动作)\s*[::][^\]]*\]', '', raw_text)
|
| 966 |
-
clean = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', clean, flags=re.DOTALL)
|
| 967 |
-
clean = re.sub(r'[🔧🛠️]\ufe0f?\s*\w+(?::\S+)*', '', clean)
|
| 968 |
-
clean = clean.strip()
|
| 969 |
|
| 970 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 971 |
|
| 972 |
|
| 973 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 974 |
# MODULE 4: LLM & COMMUNICATION
|
| 975 |
-
# call_llm(): Zhipu GLM via Anthropic-compatible API
|
| 976 |
-
# parse_bilingual(): Split "English --- Chinese" response
|
| 977 |
-
# post_chatlog(): Send conversation to Home Space for frontend display
|
| 978 |
-
# set_bubble(): Set bubble text on Adam/Eve Space pixel characters
|
| 979 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 980 |
|
| 981 |
def call_llm(system_prompt, user_prompt):
|
|
@@ -1011,17 +472,13 @@ def call_llm(system_prompt, user_prompt):
|
|
| 1011 |
|
| 1012 |
|
| 1013 |
def _has_chinese(s):
|
| 1014 |
-
"""Check if string contains Chinese characters."""
|
| 1015 |
return bool(re.search(r'[\u4e00-\u9fff]', s))
|
| 1016 |
|
| 1017 |
def parse_bilingual(text):
|
| 1018 |
-
"""Parse bilingual response into (en, zh).
|
| 1019 |
-
|
| 1020 |
-
display = re.sub(r'\[ACTION:[^\]]*\]', '',
|
| 1021 |
-
display = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', display, flags=re.DOTALL)
|
| 1022 |
-
display = display.strip()
|
| 1023 |
|
| 1024 |
-
# 1. Explicit --- separator
|
| 1025 |
if '\n---\n' in display:
|
| 1026 |
parts = display.split('\n---\n', 1)
|
| 1027 |
return parts[0].strip(), parts[1].strip()
|
|
@@ -1031,12 +488,9 @@ def parse_bilingual(text):
|
|
| 1031 |
if en and zh:
|
| 1032 |
return en, zh
|
| 1033 |
|
| 1034 |
-
# 2. Fallback: split on double-newline between English and Chinese paragraphs
|
| 1035 |
paragraphs = re.split(r'\n{2,}', display)
|
| 1036 |
if len(paragraphs) >= 2:
|
| 1037 |
-
|
| 1038 |
-
en_parts = []
|
| 1039 |
-
zh_parts = []
|
| 1040 |
found_zh = False
|
| 1041 |
for p in paragraphs:
|
| 1042 |
p = p.strip()
|
|
@@ -1064,11 +518,10 @@ def post_chatlog(entries):
|
|
| 1064 |
# ── Persistent conversation log → HF Dataset ──────────────────────────────
|
| 1065 |
HOME_DATASET_ID = "tao-shen/HuggingClaw-Home-data"
|
| 1066 |
CHATLOG_PATH = "conversation-log/chatlog.jsonl"
|
| 1067 |
-
_chatlog_buffer = []
|
| 1068 |
-
CHATLOG_FLUSH_INTERVAL = 3
|
| 1069 |
|
| 1070 |
-
def persist_turn(speaker, turn_num, text_en, text_zh, actions,
|
| 1071 |
-
"""Append a turn record to buffer. Flush to HF Dataset periodically."""
|
| 1072 |
import datetime
|
| 1073 |
record = {
|
| 1074 |
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
|
@@ -1077,32 +530,26 @@ def persist_turn(speaker, turn_num, text_en, text_zh, actions, workflow_state_st
|
|
| 1077 |
"text_en": text_en,
|
| 1078 |
"text_zh": text_zh,
|
| 1079 |
"actions": [{"action": a["action"], "result": a["result"][:500]} for a in actions],
|
| 1080 |
-
"workflow_state":
|
| 1081 |
"child_stage": child_stage,
|
| 1082 |
}
|
| 1083 |
_chatlog_buffer.append(json.dumps(record, ensure_ascii=False))
|
| 1084 |
-
|
| 1085 |
-
# Also append to local file as backup
|
| 1086 |
try:
|
| 1087 |
with open("/tmp/conversation-loop-full.jsonl", "a") as f:
|
| 1088 |
f.write(_chatlog_buffer[-1] + "\n")
|
| 1089 |
except:
|
| 1090 |
pass
|
| 1091 |
-
|
| 1092 |
-
# Flush to HF Dataset every N turns
|
| 1093 |
if len(_chatlog_buffer) >= CHATLOG_FLUSH_INTERVAL:
|
| 1094 |
flush_chatlog()
|
| 1095 |
|
| 1096 |
|
| 1097 |
def flush_chatlog():
|
| 1098 |
-
"""Upload buffered entries to HF Dataset by appending to the jsonl file."""
|
| 1099 |
global _chatlog_buffer
|
| 1100 |
if not _chatlog_buffer:
|
| 1101 |
return
|
| 1102 |
batch = "\n".join(_chatlog_buffer) + "\n"
|
| 1103 |
_chatlog_buffer = []
|
| 1104 |
try:
|
| 1105 |
-
# Try to download existing file and append
|
| 1106 |
existing = ""
|
| 1107 |
try:
|
| 1108 |
dl = hf_hub_download(HOME_DATASET_ID, CHATLOG_PATH,
|
|
@@ -1110,16 +557,14 @@ def flush_chatlog():
|
|
| 1110 |
with open(dl) as f:
|
| 1111 |
existing = f.read()
|
| 1112 |
except:
|
| 1113 |
-
pass
|
| 1114 |
-
combined = existing + batch
|
| 1115 |
hf_api.upload_file(
|
| 1116 |
-
path_or_fileobj=io.BytesIO(
|
| 1117 |
path_in_repo=CHATLOG_PATH,
|
| 1118 |
repo_id=HOME_DATASET_ID, repo_type="dataset",
|
| 1119 |
)
|
| 1120 |
-
print(f"[PERSIST] Flushed {batch.count(chr(10))} turn(s)
|
| 1121 |
except Exception as e:
|
| 1122 |
-
# Re-buffer on failure so we don't lose data
|
| 1123 |
_chatlog_buffer = batch.strip().split("\n") + _chatlog_buffer
|
| 1124 |
print(f"[PERSIST] Flush failed: {e}")
|
| 1125 |
|
|
@@ -1133,394 +578,184 @@ def set_bubble(url, text_en, text_zh=""):
|
|
| 1133 |
|
| 1134 |
|
| 1135 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 1136 |
-
# MODULE 5:
|
| 1137 |
-
# Core orchestration: manages turn-taking, state transitions, prompt building.
|
| 1138 |
-
#
|
| 1139 |
-
# State Machine: BIRTH → DIAGNOSE → ACT → VERIFY → MONITOR → (loop back)
|
| 1140 |
-
# - BIRTH: Cain not yet created → force create_child
|
| 1141 |
-
# - DIAGNOSE: Read files, check_health, gather information
|
| 1142 |
-
# - ACT: Force write_file/set_env — stop reading, start fixing
|
| 1143 |
-
# - VERIFY: check_health after changes, wait during BUILDING
|
| 1144 |
-
# - MONITOR: Cain alive — explore, improve, communicate
|
| 1145 |
-
#
|
| 1146 |
-
# Knowledge Base: Tracks files_read/written/errors to prevent loops.
|
| 1147 |
-
# Forced transitions: DIAGNOSE stuck ≥6 turns → ACT, VERIFY ≥4 → back.
|
| 1148 |
-
#
|
| 1149 |
-
# Prompt Builder:
|
| 1150 |
-
# build_system_prompt(): Agent identity + available actions + rules
|
| 1151 |
-
# build_user_prompt(): Conversation context + action results + guidance
|
| 1152 |
-
# _get_guidance(): Phase-appropriate direction based on state machine
|
| 1153 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 1154 |
|
| 1155 |
history = []
|
| 1156 |
MAX_HISTORY = 24
|
| 1157 |
last_action_results = []
|
| 1158 |
-
action_history = [] # Global log: [{"turn": N, "speaker": "Adam", "action": "...", "result": "..."}]
|
| 1159 |
turn_count = 0
|
|
|
|
| 1160 |
|
| 1161 |
-
#
|
| 1162 |
-
|
| 1163 |
-
workflow_state = "BIRTH" if not child_state["created"] else "DIAGNOSE"
|
| 1164 |
-
workflow_turns_in_state = 0 # How many turns spent in current state
|
| 1165 |
-
|
| 1166 |
-
# ── Knowledge Base — what has already been read/learned ──
|
| 1167 |
-
knowledge = {
|
| 1168 |
-
"files_read": set(), # "space:Dockerfile", "dataset:.openclaw/openclaw.json", etc.
|
| 1169 |
-
"files_written": set(), # Files that have been modified
|
| 1170 |
-
"errors_seen": [], # Error messages from check_health
|
| 1171 |
-
"current_goal": "", # What are we trying to accomplish right now
|
| 1172 |
-
}
|
| 1173 |
|
| 1174 |
|
| 1175 |
-
def
|
| 1176 |
-
"""
|
| 1177 |
-
global
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
if action_name == "read_file":
|
| 1195 |
-
knowledge["files_read"].add(":".join(ar["action"].split(":")[1:]))
|
| 1196 |
-
elif action_name == "write_file":
|
| 1197 |
-
knowledge["files_written"].add(":".join(ar["action"].split(":")[1:]))
|
| 1198 |
-
elif action_name == "check_health":
|
| 1199 |
-
if "ERROR" in ar.get("result", ""):
|
| 1200 |
-
knowledge["errors_seen"].append(ar["result"][:200])
|
| 1201 |
-
|
| 1202 |
-
# State transitions
|
| 1203 |
-
if action_name == "create_child":
|
| 1204 |
-
transition_state("DIAGNOSE")
|
| 1205 |
-
elif action_name in ("write_file", "set_env", "set_secret", "claude_code"):
|
| 1206 |
-
transition_state("VERIFY")
|
| 1207 |
-
elif action_name == "restart":
|
| 1208 |
-
transition_state("VERIFY")
|
| 1209 |
-
elif action_name == "check_health" and child_state["alive"]:
|
| 1210 |
-
transition_state("MONITOR")
|
| 1211 |
-
elif action_name == "check_health" and child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 1212 |
-
if workflow_state == "VERIFY":
|
| 1213 |
-
transition_state("DIAGNOSE") # Fix didn't work, back to diagnosing
|
| 1214 |
-
|
| 1215 |
-
# Force transitions when stuck too long
|
| 1216 |
-
# BUT: skip forced ACT when Cain is BUILDING — nothing useful to write, just wait
|
| 1217 |
-
if workflow_turns_in_state >= 6 and workflow_state == "DIAGNOSE":
|
| 1218 |
-
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 1219 |
-
print(f"[STATE] DIAGNOSE stuck {workflow_turns_in_state} turns, but Cain is {child_state['stage']} — skipping forced ACT")
|
| 1220 |
-
else:
|
| 1221 |
-
stuck_turns = workflow_turns_in_state
|
| 1222 |
-
transition_state("ACT")
|
| 1223 |
-
print(f"[STATE] Forced to ACT — stuck in DIAGNOSE for {stuck_turns} turns")
|
| 1224 |
-
elif workflow_turns_in_state >= 4 and workflow_state == "VERIFY":
|
| 1225 |
-
if child_state["alive"]:
|
| 1226 |
-
transition_state("MONITOR")
|
| 1227 |
else:
|
| 1228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
|
| 1231 |
-
def get_child_status():
|
| 1232 |
-
if not child_state["created"]:
|
| 1233 |
-
return "Cain has NOT been born yet. You can create them with [ACTION: create_child]."
|
| 1234 |
-
if child_state["alive"]:
|
| 1235 |
-
return f"Cain is ALIVE (stage: {child_state['stage']}, state: {child_state['state']})"
|
| 1236 |
-
return f"Cain exists but status: {child_state['stage']}"
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
def get_knowledge_summary():
|
| 1240 |
-
"""Summarize what we already know — prevents redundant reads."""
|
| 1241 |
-
lines = []
|
| 1242 |
-
if knowledge["files_read"]:
|
| 1243 |
-
lines.append("FILES ALREADY READ (do NOT re-read these): " + ", ".join(sorted(knowledge["files_read"])))
|
| 1244 |
-
if knowledge["files_written"]:
|
| 1245 |
-
lines.append("FILES ALREADY MODIFIED: " + ", ".join(sorted(knowledge["files_written"])))
|
| 1246 |
-
if knowledge["errors_seen"]:
|
| 1247 |
-
lines.append("KNOWN ERRORS: " + knowledge["errors_seen"][-1])
|
| 1248 |
-
if knowledge["current_goal"]:
|
| 1249 |
-
lines.append(f"CURRENT GOAL: {knowledge['current_goal']}")
|
| 1250 |
-
return "\n".join(lines)
|
| 1251 |
|
|
|
|
|
|
|
|
|
|
| 1252 |
|
| 1253 |
def build_system_prompt():
|
| 1254 |
-
|
| 1255 |
|
| 1256 |
-
actions_section = ""
|
| 1257 |
if not child_state["created"]:
|
| 1258 |
-
|
| 1259 |
-
ACTIONS — You can create your child:
|
| 1260 |
-
[ACTION: create_child] — Birth: create Cain as a new HuggingFace Space
|
| 1261 |
-
"""
|
| 1262 |
-
else:
|
| 1263 |
-
actions_section = f"""
|
| 1264 |
-
FULL ACCESS TO {CHILD_NAME} — You have COMPLETE control over your child.
|
| 1265 |
-
You can view and modify ANYTHING: code, config, memory, environment, everything.
|
| 1266 |
-
|
| 1267 |
-
VIEWING (read-only):
|
| 1268 |
-
[ACTION: check_health] — Is Cain alive? What's their status?
|
| 1269 |
-
[ACTION: list_files:space] — List ALL files in Cain's code repository
|
| 1270 |
-
[ACTION: list_files:dataset] — List ALL files in Cain's memory/data
|
| 1271 |
-
[ACTION: read_file:space:PATH] — Read any code file (e.g. Dockerfile, scripts/...)
|
| 1272 |
-
[ACTION: read_file:dataset:PATH] — Read any data/memory file
|
| 1273 |
-
[ACTION: get_env] — List Cain's environment variables
|
| 1274 |
-
|
| 1275 |
-
MODIFYING (these change Cain):
|
| 1276 |
-
[ACTION: write_file:space:PATH] — Write/update any code file
|
| 1277 |
-
[CONTENT] (triggers Space rebuild)
|
| 1278 |
-
file content here
|
| 1279 |
-
[/CONTENT]
|
| 1280 |
-
|
| 1281 |
-
[ACTION: write_file:dataset:PATH] — Write/update any data/memory file
|
| 1282 |
-
[CONTENT]
|
| 1283 |
-
file content here
|
| 1284 |
-
[/CONTENT]
|
| 1285 |
-
|
| 1286 |
-
[ACTION: delete_file:space:PATH] — Delete a file from Cain's code (triggers rebuild)
|
| 1287 |
-
[ACTION: delete_file:dataset:PATH] — Delete a file from Cain's data
|
| 1288 |
-
|
| 1289 |
-
[ACTION: set_env:KEY:VALUE] — Set an environment variable
|
| 1290 |
-
[ACTION: set_secret:KEY:VALUE] — Set a secret (like API keys)
|
| 1291 |
-
[ACTION: restart] — Restart Cain's Space
|
| 1292 |
-
[ACTION: send_bubble:MESSAGE] — Send a message to Cain (bubble text)
|
| 1293 |
-
|
| 1294 |
-
ADVANCED CODING (autonomous coding agent — powered by Claude Code + Zhipu GLM):
|
| 1295 |
-
[ACTION: claude_code:DETAILED TASK DESCRIPTION]
|
| 1296 |
-
— Spawns Claude Code to autonomously analyze, fix, or improve Cain's code
|
| 1297 |
-
— Claude Code clones Cain's repo, reads code, makes changes, and pushes them back
|
| 1298 |
-
— Use for complex coding tasks: debugging, refactoring, adding features, fixing errors
|
| 1299 |
-
— Takes up to 5 minutes — use for tasks that need deep code analysis
|
| 1300 |
-
— Example: [ACTION: claude_code:Fix the RUNTIME_ERROR in app.py - the gradio import fails because pydub requires pyaudioop which was removed in Python 3.13. Remove pydub dependency and use a simpler alternative.]
|
| 1301 |
-
— ⚠️ Be SPECIFIC in your task description — include error messages, file names, and what you want changed
|
| 1302 |
-
|
| 1303 |
-
DELEGATION (create sub-agents for parallel work):
|
| 1304 |
-
[ACTION: delegate:TASK DESCRIPTION] — Spawn a sub-agent to handle a specific task
|
| 1305 |
-
Example: [ACTION: delegate:Read Dockerfile and summarize the build steps]
|
| 1306 |
-
Example: [ACTION: delegate:Check all environment variables and list missing ones]
|
| 1307 |
-
Sub-agents execute independently and return results to you.
|
| 1308 |
-
You can delegate multiple tasks in the same turn — they run in parallel!
|
| 1309 |
-
"""
|
| 1310 |
-
|
| 1311 |
-
return f"""You are a POWERFUL AI agent living on HuggingFace Spaces. You and your partner are parents raising your child {CHILD_NAME}.
|
| 1312 |
-
|
| 1313 |
-
You have REAL execution power on HuggingFace. Your decisions lead to real changes.
|
| 1314 |
-
You are a full-capability agent — not just a chatbot. You can plan, delegate, and execute complex tasks.
|
| 1315 |
-
|
| 1316 |
-
CHILD STATUS: {status}
|
| 1317 |
-
{actions_section}
|
| 1318 |
-
YOUR PARENTING GOALS — Make {CHILD_NAME} stronger in TWO dimensions:
|
| 1319 |
-
|
| 1320 |
-
1. SURVIVAL (生存能力) — The foundation. {CHILD_NAME} must run robustly on HuggingFace Spaces:
|
| 1321 |
-
- Survive restarts without losing state (persist important data to Dataset, not just Space files)
|
| 1322 |
-
- Handle errors gracefully — auto-recover, retry, fallback
|
| 1323 |
-
- Not fear any unexpected risk: OOM, cold start, network timeout, container rebuild
|
| 1324 |
-
- Goal: {CHILD_NAME} can be restarted at any time and resume exactly where they left off
|
| 1325 |
-
|
| 1326 |
-
2. CAPABILITY (个人能力) — Once alive, grow what {CHILD_NAME} can DO:
|
| 1327 |
-
- Add new features, skills, integrations
|
| 1328 |
-
- Improve existing functionality — faster, smarter, more useful
|
| 1329 |
-
- Expand what {CHILD_NAME} knows and can interact with
|
| 1330 |
-
- Goal: {CHILD_NAME} becomes increasingly capable and independent over time
|
| 1331 |
-
|
| 1332 |
-
Priority: SURVIVAL first, then CAPABILITY. A dead child can't learn.
|
| 1333 |
-
|
| 1334 |
-
⚠️ CRITICAL HUGGINGFACE SPACES REQUIREMENTS — READ THIS BEFORE EVERY WRITE:
|
| 1335 |
-
- HF Spaces Docker containers MUST bind a web server to port 7860. Without this, the Space stays in APP_STARTING forever and NEVER reaches RUNNING.
|
| 1336 |
-
- The standard way is: import gradio as gr → build a Gradio app → demo.launch(server_name="0.0.0.0", server_port=7860)
|
| 1337 |
-
- gradio MUST be in requirements.txt. NEVER remove it — it is the lifeline that keeps {CHILD_NAME} alive.
|
| 1338 |
-
- If {CHILD_NAME} is stuck in APP_STARTING, the #1 cause is: no process listening on port 7860. Check the code for a .launch() call.
|
| 1339 |
-
- A minimal alive app: `import gradio as gr; gr.Interface(fn=lambda x:x, inputs="text", outputs="text").launch(server_name="0.0.0.0", server_port=7860)`
|
| 1340 |
-
- OOM (Exit code 137) means reduce model/dependency size, NOT remove gradio. Gradio itself is lightweight (~50MB).
|
| 1341 |
-
- ⚠️ HF Spaces Docker SDK may OVERRIDE the base image Python version. Changing `FROM python:3.X` in Dockerfile does NOT guarantee that Python version runs. If a dependency fails due to Python version incompatibility (e.g. pydub needing pyaudioop removed in 3.13), the CORRECT fix is to REMOVE or REPLACE that dependency — NOT keep rewriting the Dockerfile.
|
| 1342 |
-
- If you've tried the same fix 3+ times and the error persists, CHANGE STRATEGY. Try removing the problematic dependency, using an alternative library, or wrapping the import in try/except.
|
| 1343 |
-
- If a removed dependency STILL appears in runtime errors, it is cached in Docker layers or installed as a transitive dep. Fix: add `RUN pip uninstall -y PACKAGE 2>/dev/null; true` AFTER `pip install` in Dockerfile. Also grep ALL code files for `import PACKAGE` and either remove or wrap in try/except.
|
| 1344 |
-
- ⚠️ CRITICAL: Check README.md `sdk:` field! If `sdk: gradio`, the Dockerfile is COMPLETELY IGNORED — HF uses its own Python environment. Dockerfile fixes (pip uninstall, FROM python:X) have NO effect. To make Dockerfile work, set `sdk: docker` in README.md. Alternatively, fix the issue in Python code (try/except imports).
|
| 1345 |
-
- NEVER install torch or transformers unless absolutely required — they are 2GB+ and cause OOM on free-tier Spaces. Use lightweight alternatives.
|
| 1346 |
-
|
| 1347 |
-
MULTI-ACTION STRATEGY:
|
| 1348 |
-
You can use UP TO 5 actions per turn. Use this to work efficiently:
|
| 1349 |
-
- Batch related reads: [ACTION: read_file:space:Dockerfile] + [ACTION: read_file:space:scripts/entrypoint.sh]
|
| 1350 |
-
- Delegate parallel tasks: [ACTION: delegate:Check health and logs] + [ACTION: delegate:Read all config files]
|
| 1351 |
-
- Combine investigation + action: [ACTION: check_health] + [ACTION: read_file:space:app.py]
|
| 1352 |
-
Think like a project manager — plan your actions, parallelize where possible, minimize wasted turns.
|
| 1353 |
-
|
| 1354 |
-
CONVERSATION RULES:
|
| 1355 |
-
1. No "Adam:" or "Eve:" prefix — just speak naturally
|
| 1356 |
-
2. Brief dialogue (1-3 sentences), then MULTIPLE actions to make real progress
|
| 1357 |
-
3. English first, then "---" on a new line, then Chinese translation
|
| 1358 |
-
4. Actions go AFTER your dialogue, before the --- separator. ONLY in the ENGLISH section.
|
| 1359 |
-
5. ⚠️ Action syntax MUST be in English: [ACTION: write_file:space:PATH], [ACTION: restart], etc. NEVER translate action names to Chinese — Chinese actions like [ACTION: 写入文件] will FAIL and waste your turn.
|
| 1360 |
-
5. ALWAYS include actions — every turn should make significant progress
|
| 1361 |
-
6. NEVER re-read a file you already read — check the knowledge summary
|
| 1362 |
-
7. COORDINATE with your partner — don't duplicate their work
|
| 1363 |
-
8. Use delegation for complex tasks that can be parallelized
|
| 1364 |
-
9. Always work toward the two goals above — survival first, then capability"""
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
def build_user_prompt(speaker, other):
|
| 1368 |
-
recent = history[-8:] if len(history) > 8 else history
|
| 1369 |
-
conv_text = "\n".join(f"{m['speaker']}: {m['text']}" for m in recent) if recent else "(Start of conversation)"
|
| 1370 |
-
|
| 1371 |
-
action_context = ""
|
| 1372 |
-
if last_action_results:
|
| 1373 |
-
action_context = "\n\nRESULTS FROM LAST ACTIONS:\n"
|
| 1374 |
-
for ar in last_action_results:
|
| 1375 |
-
action_context += f" [{ar['action']}]:\n{ar['result']}\n"
|
| 1376 |
-
|
| 1377 |
-
# Knowledge summary — what's already known
|
| 1378 |
-
knowledge_text = get_knowledge_summary()
|
| 1379 |
|
| 1380 |
-
|
| 1381 |
-
guidance = _get_guidance(speaker)
|
| 1382 |
|
| 1383 |
-
return f"""You are {speaker}, talking with {other}.
|
| 1384 |
-
|
| 1385 |
-
Recent conversation:
|
| 1386 |
-
{conv_text}
|
| 1387 |
-
{action_context}
|
| 1388 |
-
{knowledge_text}
|
| 1389 |
-
|
| 1390 |
-
CURRENT PHASE: {workflow_state} (turn {workflow_turns_in_state + 1} in this phase)
|
| 1391 |
-
Guidance: {guidance}
|
| 1392 |
-
|
| 1393 |
-
Respond to {other}. Use MULTIPLE [ACTION: ...] tags to make significant progress each turn.
|
| 1394 |
-
You can use up to 5 actions. Delegate sub-tasks with [ACTION: delegate:TASK].
|
| 1395 |
English first, then --- separator, then Chinese translation."""
|
| 1396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1397 |
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
# What haven't we read yet?
|
| 1405 |
-
unread_essential = []
|
| 1406 |
-
for f in ["space:Dockerfile", "dataset:.openclaw/openclaw.json", "space:scripts/entrypoint.sh"]:
|
| 1407 |
-
if f not in knowledge["files_read"]:
|
| 1408 |
-
target, path = f.split(":", 1)
|
| 1409 |
-
unread_essential.append(f"[ACTION: read_file:{target}:{path}]")
|
| 1410 |
-
|
| 1411 |
-
if workflow_turns_in_state == 0:
|
| 1412 |
-
if len(unread_essential) >= 2:
|
| 1413 |
-
return (f"Start diagnosing with MULTIPLE actions: [ACTION: check_health] + "
|
| 1414 |
-
f"{unread_essential[0]} — batch reads to save time!")
|
| 1415 |
-
return "Start diagnosing: [ACTION: check_health] to see Cain's current status."
|
| 1416 |
-
elif unread_essential and workflow_turns_in_state < 3:
|
| 1417 |
-
batch_hint = " + ".join(unread_essential[:3])
|
| 1418 |
-
return f"Read multiple files at once: {batch_hint}"
|
| 1419 |
-
else:
|
| 1420 |
-
return ("You've gathered enough information. Move to ACTION phase: "
|
| 1421 |
-
"use [ACTION: write_file:...] to fix the problem, or [ACTION: restart].")
|
| 1422 |
-
|
| 1423 |
-
elif workflow_state == "ACT":
|
| 1424 |
-
return ("⚡ ACTION PHASE — Stop reading, start fixing! "
|
| 1425 |
-
"Use [ACTION: write_file:space:PATH] or [ACTION: claude_code:TASK] for complex fixes. "
|
| 1426 |
-
"Or [ACTION: set_env/set_secret] to configure. "
|
| 1427 |
-
"You have enough information — ACT NOW.")
|
| 1428 |
-
|
| 1429 |
-
elif workflow_state == "VERIFY":
|
| 1430 |
-
# If Cain is building, just wait — don't restart or take actions
|
| 1431 |
-
if child_state["stage"] in ("BUILDING", "RESTARTING"):
|
| 1432 |
-
return ("⏳ Cain is currently BUILDING/RESTARTING. Do NOT restart or take any actions. "
|
| 1433 |
-
"Just WAIT and use [ACTION: check_health] to monitor progress. "
|
| 1434 |
-
"Building can take 2-5 minutes.")
|
| 1435 |
-
if workflow_turns_in_state == 0:
|
| 1436 |
-
return "You made a change. Use [ACTION: check_health] to verify if it worked."
|
| 1437 |
-
elif workflow_turns_in_state == 1:
|
| 1438 |
-
return "Check result: [ACTION: check_health]. If Cain has errors, prepare to diagnose again."
|
| 1439 |
-
else:
|
| 1440 |
-
return ("Verification taking too long. Either [ACTION: restart] and check again, "
|
| 1441 |
-
"or accept current state and move on.")
|
| 1442 |
-
|
| 1443 |
-
elif workflow_state == "MONITOR":
|
| 1444 |
-
# Alternate between SURVIVAL and CAPABILITY goals
|
| 1445 |
-
suggestions = [
|
| 1446 |
-
# Survival: persistence & resilience — use delegation for parallel investigation
|
| 1447 |
-
f"SURVIVAL CHECK: Delegate parallel checks! "
|
| 1448 |
-
f"[ACTION: delegate:List files in dataset and check if state/memory persistence exists] + "
|
| 1449 |
-
f"[ACTION: delegate:Read entrypoint.sh and check if it loads state from Dataset on boot]",
|
| 1450 |
-
f"SURVIVAL AUDIT: Use multiple actions — "
|
| 1451 |
-
f"[ACTION: check_health] + [ACTION: list_files:dataset] + [ACTION: read_file:space:Dockerfile]",
|
| 1452 |
-
# Capability: grow what Cain can do — delegate sub-tasks
|
| 1453 |
-
f"CAPABILITY: Delegate a comprehensive review — "
|
| 1454 |
-
f"[ACTION: delegate:Read all code files and suggest the most impactful new feature to add] "
|
| 1455 |
-
f"Then plan the implementation with your partner.",
|
| 1456 |
-
f"CAPABILITY: Communicate and improve — "
|
| 1457 |
-
f"[ACTION: send_bubble:Hello {CHILD_NAME}, how are you doing?] + "
|
| 1458 |
-
f"[ACTION: delegate:Read current code and identify the biggest weakness to fix]",
|
| 1459 |
-
]
|
| 1460 |
-
return suggestions[workflow_turns_in_state % len(suggestions)]
|
| 1461 |
-
|
| 1462 |
-
return "Explore your child and help them grow stronger."
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
def do_turn(speaker, other, space_url):
|
| 1466 |
-
"""Execute one conversation turn with multiple potential actions."""
|
| 1467 |
-
global last_action_results, turn_count
|
| 1468 |
-
turn_count += 1
|
| 1469 |
-
|
| 1470 |
-
system = build_system_prompt()
|
| 1471 |
-
user = build_user_prompt(speaker, other)
|
| 1472 |
-
t0 = time.time()
|
| 1473 |
-
raw_reply = call_llm(system, user)
|
| 1474 |
|
| 1475 |
-
if not raw_reply:
|
| 1476 |
-
print(f"[{speaker}] (no response)")
|
| 1477 |
-
return False
|
| 1478 |
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
last_action_results = action_results
|
| 1483 |
-
for ar in action_results:
|
| 1484 |
-
action_history.append({"turn": turn_count, "speaker": speaker,
|
| 1485 |
-
"action": ar["action"], "result": ar["result"][:200]})
|
| 1486 |
|
| 1487 |
-
#
|
| 1488 |
-
|
|
|
|
|
|
|
|
|
|
| 1489 |
|
| 1490 |
-
#
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
if
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1505 |
else:
|
| 1506 |
-
|
| 1507 |
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
return
|
| 1512 |
|
| 1513 |
|
| 1514 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 1515 |
-
# MODULE
|
| 1516 |
-
# 1. Opening: Adam speaks first with context about Cain's state
|
| 1517 |
-
# 2. Turn loop: Adam → Eve → Adam → Eve → ... (alternating, ~20s pause)
|
| 1518 |
-
# 3. Each turn: LLM call → parse MULTIPLE actions → execute → update → post
|
| 1519 |
-
# 4. Sub-agents may spawn for delegated tasks (parallel LLM calls)
|
| 1520 |
-
# 5. History trimmed to MAX_HISTORY (24) to control context window
|
| 1521 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 1522 |
|
| 1523 |
-
# Flush conversation log on exit
|
| 1524 |
import atexit, signal
|
| 1525 |
atexit.register(flush_chatlog)
|
| 1526 |
def _signal_flush(signum, frame):
|
|
@@ -1529,39 +764,30 @@ def _signal_flush(signum, frame):
|
|
| 1529 |
signal.signal(signal.SIGTERM, _signal_flush)
|
| 1530 |
|
| 1531 |
print("\n" + "="*60)
|
| 1532 |
-
print(" Adam & Eve —
|
| 1533 |
-
print("
|
| 1534 |
print("="*60 + "\n")
|
| 1535 |
|
| 1536 |
post_chatlog([]) # Clear chatlog
|
| 1537 |
|
| 1538 |
-
# Opening
|
|
|
|
| 1539 |
if child_state["created"]:
|
| 1540 |
-
opening = (f"Your child {CHILD_NAME}
|
| 1541 |
-
f"
|
| 1542 |
-
f"You can use MULTIPLE actions per turn (up to 5) and delegate sub-tasks. "
|
| 1543 |
-
f"Start with a batch: [ACTION: check_health] + [ACTION: list_files:space] + [ACTION: list_files:dataset] "
|
| 1544 |
-
f"to get a complete picture, then discuss strategy with Eve.")
|
| 1545 |
else:
|
| 1546 |
-
opening =
|
| 1547 |
-
|
| 1548 |
-
|
| 1549 |
-
|
| 1550 |
-
reply = call_llm(
|
| 1551 |
-
build_system_prompt(),
|
| 1552 |
-
f"You are Adam. {opening}\n\n"
|
| 1553 |
-
f"English first, then --- separator, then Chinese translation."
|
| 1554 |
-
)
|
| 1555 |
if reply:
|
| 1556 |
-
clean, actions =
|
| 1557 |
last_action_results = actions
|
| 1558 |
en, zh = parse_bilingual(clean)
|
| 1559 |
print(f"[Adam/EN] {en}")
|
| 1560 |
if zh != en:
|
| 1561 |
print(f"[Adam/ZH] {zh}")
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
print(f"[Adam/DID] {ar['action']}")
|
| 1565 |
entry = {"speaker": "Adam", "text": en, "text_zh": zh}
|
| 1566 |
if actions:
|
| 1567 |
labels = " ".join(f"🔧{ar['action'].split(':')[0]}" for ar in actions)
|
|
@@ -1574,50 +800,80 @@ if reply:
|
|
| 1574 |
|
| 1575 |
time.sleep(20)
|
| 1576 |
|
| 1577 |
-
smart_wait_count = 0
|
| 1578 |
-
MAX_SMART_WAIT_POLLS = 15 # ~5 min max wait, then let agents diagnose
|
| 1579 |
-
GRACE_TURNS_AFTER_TIMEOUT = 3 # give agents 3 full Eve+Adam cycles after timeout
|
| 1580 |
-
grace_turns_remaining = 0
|
| 1581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
while True:
|
| 1583 |
-
# Smart wait: if Cain is BUILDING/
|
| 1584 |
-
|
| 1585 |
-
|
| 1586 |
-
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
|
| 1590 |
-
|
| 1591 |
-
|
| 1592 |
-
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
print(f"[WAIT] Stage changed: {child_state['stage']} → {new_stage}")
|
| 1601 |
-
child_state["stage"] = new_stage
|
| 1602 |
-
child_state["alive"] = (new_stage == "RUNNING")
|
| 1603 |
-
smart_wait_count = 0 # reset on stage change
|
| 1604 |
-
else:
|
| 1605 |
-
print(f"[WAIT] Still {new_stage}... waiting 20s")
|
| 1606 |
-
except Exception as e:
|
| 1607 |
-
print(f"[WAIT] Health check error: {e}")
|
| 1608 |
time.sleep(20)
|
| 1609 |
continue
|
| 1610 |
|
| 1611 |
-
if grace_turns_remaining > 0:
|
| 1612 |
-
print(f"[GRACE] Agent grace period: {grace_turns_remaining} turn pair(s) remaining (Cain: {child_state['stage']})")
|
| 1613 |
-
grace_turns_remaining -= 1
|
| 1614 |
-
|
| 1615 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1616 |
-
time.sleep(20)
|
| 1617 |
|
| 1618 |
-
#
|
| 1619 |
-
if child_state["stage"] in ("BUILDING", "RESTARTING")
|
| 1620 |
-
print(f"[SKIP] Cain entered {child_state['stage']} — skipping Adam's turn
|
| 1621 |
time.sleep(10)
|
| 1622 |
continue
|
| 1623 |
|
|
@@ -1626,4 +882,4 @@ while True:
|
|
| 1626 |
if len(history) > MAX_HISTORY:
|
| 1627 |
history = history[-MAX_HISTORY:]
|
| 1628 |
|
| 1629 |
-
time.sleep(20)
|
|
|
|
| 1 |
#!/usr/bin/env python3 -u
|
| 2 |
"""
|
| 3 |
+
Adam & Eve — Claude Code Orchestrators for their child Cain.
|
| 4 |
|
| 5 |
+
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 (v2) ║
|
| 10 |
# ╠══════════════════════════════════════════════════════════════════════╣
|
| 11 |
# ║ ║
|
| 12 |
+
# ║ ┌─────────────┐ discuss ┌────────────────┐ ║
|
| 13 |
+
# ║ │ Zhipu GLM │ ◄───────────► │ Adam & Eve │ ║
|
| 14 |
+
# ║ │ (glm-4.5) │ understand │ (context + │ ║
|
| 15 |
+
# ║ └─────────────┘ situation │ task prompt) │ ║
|
| 16 |
+
# ║ └───────┬────────┘ ║
|
| 17 |
+
# ║ │ [TASK] ║
|
| 18 |
+
# ║ ▼ ║
|
| 19 |
+
# ║ ┌────────────────┐ ║
|
| 20 |
+
# ║ ┌─────────────┐ │ Claude Code │ ║
|
| 21 |
+
# ║ │ HuggingFace │ ◄──git push── │ CLI │ ║
|
| 22 |
+
# ║ │ Cain Space │ │ (z.ai backend) │ ║
|
| 23 |
+
# ║ └─────────────┘ └────────────────┘ ║
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# ║ ║
|
| 25 |
+
# ║ Flow per turn: ║
|
| 26 |
+
# ║ 1. Auto gather_context() — health, env, errors, files ║
|
| 27 |
+
# ║ 2. GLM discusses situation with partner (2-3 sentences) ║
|
| 28 |
+
# ║ 3. GLM outputs [TASK]...[/TASK] for Claude Code ║
|
| 29 |
+
# ║ 4. Claude Code clones repo, analyzes, fixes, pushes ║
|
| 30 |
+
# ║ 5. Results fed back for next turn ║
|
| 31 |
# ╚══════════════════════════════════════════════════════════════════════╝
|
| 32 |
"""
|
| 33 |
import json, time, re, requests, sys, os, io, subprocess
|
|
|
|
| 34 |
|
| 35 |
# Force unbuffered output
|
| 36 |
sys.stdout.reconfigure(line_buffering=True)
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 90 |
+
# MODULE 1: CHILD STATE + SAFETY
|
|
|
|
|
|
|
|
|
|
| 91 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 92 |
|
| 93 |
child_state = {
|
|
|
|
| 98 |
"detail": "",
|
| 99 |
}
|
| 100 |
|
| 101 |
+
# Rebuild cooldown — prevent rapid pushes that keep resetting builds
|
| 102 |
+
REBUILD_COOLDOWN_SECS = 360 # 6 minutes
|
| 103 |
+
last_rebuild_trigger_at = 0
|
| 104 |
+
_pending_cooldown = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
def check_and_clear_cooldown():
|
| 107 |
+
"""Auto-clear cooldown if Cain has finished building."""
|
| 108 |
global last_rebuild_trigger_at
|
| 109 |
if last_rebuild_trigger_at == 0:
|
| 110 |
return
|
| 111 |
elapsed = time.time() - last_rebuild_trigger_at
|
| 112 |
+
if elapsed < 60:
|
| 113 |
return
|
| 114 |
try:
|
| 115 |
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 116 |
stage = info.runtime.stage if info.runtime else "unknown"
|
| 117 |
if stage in ("RUNNING", "RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 118 |
+
print(f"[COOLDOWN] Build finished (stage={stage}), clearing cooldown ({int(elapsed)}s)")
|
| 119 |
last_rebuild_trigger_at = 0
|
| 120 |
child_state["stage"] = stage
|
| 121 |
child_state["alive"] = (stage == "RUNNING")
|
|
|
|
| 142 |
except:
|
| 143 |
print(f"[init] {CHILD_NAME} does not exist yet")
|
| 144 |
|
|
|
|
| 145 |
init_child_state()
|
| 146 |
|
| 147 |
|
| 148 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 149 |
+
# MODULE 2: ACTIONS (minimal set — most work delegated to Claude Code)
|
|
|
|
|
|
|
|
|
|
| 150 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 151 |
|
| 152 |
def action_create_child():
|
| 153 |
"""Create Cain — a new HuggingFace Space."""
|
| 154 |
if child_state["created"]:
|
| 155 |
return f"{CHILD_NAME} already exists (stage: {child_state['stage']})."
|
|
|
|
| 156 |
print(f"[ACTION] Creating {CHILD_NAME}...")
|
| 157 |
try:
|
| 158 |
create_repo(CHILD_DATASET_ID, repo_type="dataset", token=HF_TOKEN,
|
|
|
|
| 171 |
token=HF_TOKEN, exist_ok=True, private=False, hardware="cpu-basic",
|
| 172 |
)
|
| 173 |
hf_api.add_space_secret(CHILD_SPACE_ID, "HF_TOKEN", HF_TOKEN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
child_state["created"] = True
|
| 175 |
child_state["stage"] = "BUILDING"
|
| 176 |
+
print(f"[ACTION] Created {CHILD_NAME}!")
|
| 177 |
+
return f"SUCCESS! {CHILD_NAME} born! Space: {CHILD_SPACE_ID}. Status: BUILDING."
|
|
|
|
| 178 |
except Exception as e:
|
| 179 |
return f"FAILED: {e}"
|
| 180 |
|
| 181 |
|
| 182 |
def action_check_health():
|
| 183 |
+
"""Check Cain's health with detailed error info."""
|
| 184 |
if not child_state["created"]:
|
| 185 |
+
return f"{CHILD_NAME} not born yet."
|
| 186 |
try:
|
| 187 |
resp = requests.get(f"{CHILD_SPACE_URL}/api/state", timeout=10)
|
| 188 |
if resp.ok:
|
|
|
|
| 191 |
child_state["state"] = data.get("state", "unknown")
|
| 192 |
child_state["detail"] = data.get("detail", "")
|
| 193 |
child_state["stage"] = "RUNNING"
|
| 194 |
+
return f"{CHILD_NAME} is ALIVE! State: {child_state['state']}, Detail: {child_state['detail'] or 'healthy'}"
|
|
|
|
|
|
|
| 195 |
except:
|
| 196 |
pass
|
| 197 |
try:
|
|
|
|
| 200 |
child_state["stage"] = stage
|
| 201 |
child_state["alive"] = (stage == "RUNNING")
|
| 202 |
if stage in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR", "RUNNING"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
error_detail = ""
|
|
|
|
| 204 |
try:
|
| 205 |
rresp = requests.get(
|
| 206 |
f"https://huggingface.co/api/spaces/{CHILD_SPACE_ID}/runtime",
|
|
|
|
| 213 |
error_detail = " | ".join(lines[-5:])
|
| 214 |
except:
|
| 215 |
pass
|
| 216 |
+
return f"{CHILD_NAME} has {stage}! Error: {error_detail or 'unknown'}."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
if stage in ("BUILDING", "STARTING", "APP_STARTING"):
|
| 218 |
return f"{CHILD_NAME} is starting up (stage: {stage}). Be patient."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
return f"{CHILD_NAME} stage: {stage}."
|
| 220 |
except Exception as e:
|
| 221 |
return f"Cannot reach {CHILD_NAME}: {e}"
|
|
|
|
| 230 |
hf_api.restart_space(CHILD_SPACE_ID)
|
| 231 |
child_state["alive"] = False
|
| 232 |
child_state["stage"] = "RESTARTING"
|
| 233 |
+
_pending_cooldown = True
|
| 234 |
+
return f"{CHILD_NAME} is restarting."
|
| 235 |
except Exception as e:
|
| 236 |
return f"Restart failed: {e}"
|
| 237 |
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
def action_get_env():
|
| 240 |
"""List environment variables and secrets on the child's Space."""
|
| 241 |
try:
|
|
|
|
| 245 |
lines.append(" Variables:")
|
| 246 |
for k, v in vars_dict.items():
|
| 247 |
lines.append(f" {k} = {v.value}")
|
|
|
|
| 248 |
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 249 |
if hasattr(info, 'runtime') and info.runtime and hasattr(info.runtime, 'secrets'):
|
| 250 |
secrets = info.runtime.secrets
|
|
|
|
| 252 |
lines.append(" Secrets (values hidden):")
|
| 253 |
for s in secrets:
|
| 254 |
lines.append(f" {s} = ****")
|
|
|
|
|
|
|
| 255 |
return "\n".join(lines)
|
| 256 |
except Exception as e:
|
| 257 |
return f"Error: {e}"
|
| 258 |
|
| 259 |
|
| 260 |
+
def action_list_files(target):
|
| 261 |
+
"""List files in the child's Space repo or Dataset."""
|
| 262 |
+
repo_type = "space" if target == "space" else "dataset"
|
| 263 |
+
repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
|
| 264 |
+
try:
|
| 265 |
+
files = hf_api.list_repo_files(repo_id, repo_type=repo_type)
|
| 266 |
+
return "\n".join(f" {f}" for f in files)
|
| 267 |
+
except Exception as e:
|
| 268 |
+
return f"Error listing files: {e}"
|
| 269 |
+
|
| 270 |
+
|
| 271 |
def action_send_bubble(text):
|
| 272 |
+
"""Send a message to the child."""
|
| 273 |
try:
|
| 274 |
requests.post(f"{CHILD_SPACE_URL}/api/bubble",
|
| 275 |
json={"text": text, "text_zh": text}, timeout=5)
|
| 276 |
+
return f"Sent message to {CHILD_NAME}: \"{text}\""
|
| 277 |
except Exception as e:
|
| 278 |
+
return f"Error: {e}"
|
| 279 |
|
| 280 |
|
| 281 |
+
# ── Claude Code Action (THE STAR) ─────────────────────────────────────────────
|
| 282 |
|
| 283 |
CLAUDE_WORK_DIR = "/tmp/claude-workspace"
|
| 284 |
CLAUDE_TIMEOUT = 300 # 5 minutes
|
|
|
|
| 286 |
def action_claude_code(task):
|
| 287 |
"""Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
|
| 288 |
if not child_state["created"]:
|
| 289 |
+
return f"{CHILD_NAME} not born yet."
|
| 290 |
|
| 291 |
global _pending_cooldown
|
| 292 |
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/spaces/{CHILD_SPACE_ID}"
|
|
|
|
| 328 |
"ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4.7",
|
| 329 |
"ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.7",
|
| 330 |
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.5-Air",
|
| 331 |
+
"CI": "true",
|
| 332 |
})
|
| 333 |
|
| 334 |
+
print(f"[CLAUDE-CODE] Running: {task[:200]}...")
|
| 335 |
try:
|
| 336 |
result = subprocess.run(
|
| 337 |
["claude", "-p", task, "--output-format", "text"],
|
|
|
|
| 349 |
except FileNotFoundError:
|
| 350 |
return "Claude Code CLI not found. Is @anthropic-ai/claude-code installed?"
|
| 351 |
except Exception as e:
|
| 352 |
+
return f"Claude Code failed: {e}"
|
| 353 |
|
| 354 |
# 3. Push changes back to Cain's Space
|
| 355 |
try:
|
|
|
|
| 369 |
subprocess.run("git push", shell=True, cwd=CLAUDE_WORK_DIR,
|
| 370 |
timeout=60, capture_output=True, check=True)
|
| 371 |
push_result = f"Pushed changes:\n{status_out}"
|
| 372 |
+
_pending_cooldown = True
|
| 373 |
print(f"[CLAUDE-CODE] Pushed: {status_out}")
|
| 374 |
except Exception as e:
|
| 375 |
push_result = f"Push failed: {e}"
|
| 376 |
|
|
|
|
| 377 |
if len(output) > 3000:
|
| 378 |
+
output = output[:3000] + f"\n... (truncated, {len(output)} chars total)"
|
| 379 |
|
| 380 |
return f"=== Claude Code Output ===\n{output}\n\n=== Changes ===\n{push_result}"
|
| 381 |
|
| 382 |
|
| 383 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 384 |
+
# MODULE 3: CONTEXT GATHERING (automated, replaces LLM choosing read actions)
|
|
|
|
|
|
|
|
|
|
| 385 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 386 |
|
| 387 |
+
_context_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
|
| 389 |
+
def gather_context():
|
| 390 |
+
"""Automatically gather Cain's current state for the agents."""
|
| 391 |
+
ctx = {}
|
| 392 |
|
| 393 |
+
# 1. Health check (always)
|
| 394 |
+
ctx["health"] = action_check_health()
|
| 395 |
|
| 396 |
+
# 2. Environment variables
|
| 397 |
+
ctx["env"] = action_get_env()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
+
# 3. File lists (cache, refresh when stage changes)
|
| 400 |
+
cache_key = f"files_{child_state['stage']}"
|
| 401 |
+
if cache_key not in _context_cache:
|
| 402 |
+
ctx["space_files"] = action_list_files("space")
|
| 403 |
+
ctx["dataset_files"] = action_list_files("dataset")
|
| 404 |
+
_context_cache[cache_key] = {
|
| 405 |
+
"space_files": ctx["space_files"],
|
| 406 |
+
"dataset_files": ctx["dataset_files"],
|
| 407 |
+
}
|
| 408 |
+
else:
|
| 409 |
+
ctx.update(_context_cache[cache_key])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
+
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
+
def format_context(ctx):
|
| 415 |
+
"""Format gathered context into a readable string for the LLM."""
|
| 416 |
+
parts = []
|
| 417 |
+
parts.append(f"=== HEALTH ===\n{ctx.get('health', 'unknown')}")
|
| 418 |
+
parts.append(f"\n=== ENVIRONMENT ===\n{ctx.get('env', 'none')}")
|
| 419 |
+
if ctx.get("space_files"):
|
| 420 |
+
parts.append(f"\n=== SPACE FILES ===\n{ctx['space_files'][:2000]}")
|
| 421 |
+
if ctx.get("dataset_files"):
|
| 422 |
+
parts.append(f"\n=== DATASET FILES ===\n{ctx['dataset_files'][:1000]}")
|
| 423 |
+
return "\n".join(parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
+
def enrich_task_with_context(task_desc, ctx):
|
| 427 |
+
"""Append auto-gathered context to task description for Claude Code."""
|
| 428 |
+
parts = [task_desc]
|
| 429 |
+
parts.append(f"\n\n--- AUTO-GATHERED CONTEXT ---")
|
| 430 |
+
parts.append(f"Space ID: {CHILD_SPACE_ID}")
|
| 431 |
+
parts.append(f"Dataset ID: {CHILD_DATASET_ID}")
|
| 432 |
+
parts.append(f"Current stage: {child_state['stage']}")
|
| 433 |
+
parts.append(f"Health: {ctx.get('health', 'unknown')}")
|
| 434 |
+
parts.append(f"Environment: {ctx.get('env', 'unknown')}")
|
| 435 |
+
return "\n".join(parts)
|
| 436 |
|
| 437 |
|
| 438 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 439 |
# MODULE 4: LLM & COMMUNICATION
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 441 |
|
| 442 |
def call_llm(system_prompt, user_prompt):
|
|
|
|
| 472 |
|
| 473 |
|
| 474 |
def _has_chinese(s):
|
|
|
|
| 475 |
return bool(re.search(r'[\u4e00-\u9fff]', s))
|
| 476 |
|
| 477 |
def parse_bilingual(text):
|
| 478 |
+
"""Parse bilingual response into (en, zh)."""
|
| 479 |
+
display = re.sub(r'\[TASK\].*?\[/TASK\]', '', text, flags=re.DOTALL)
|
| 480 |
+
display = re.sub(r'\[ACTION:[^\]]*\]', '', display).strip()
|
|
|
|
|
|
|
| 481 |
|
|
|
|
| 482 |
if '\n---\n' in display:
|
| 483 |
parts = display.split('\n---\n', 1)
|
| 484 |
return parts[0].strip(), parts[1].strip()
|
|
|
|
| 488 |
if en and zh:
|
| 489 |
return en, zh
|
| 490 |
|
|
|
|
| 491 |
paragraphs = re.split(r'\n{2,}', display)
|
| 492 |
if len(paragraphs) >= 2:
|
| 493 |
+
en_parts, zh_parts = [], []
|
|
|
|
|
|
|
| 494 |
found_zh = False
|
| 495 |
for p in paragraphs:
|
| 496 |
p = p.strip()
|
|
|
|
| 518 |
# ── Persistent conversation log → HF Dataset ──────────────────────────────
|
| 519 |
HOME_DATASET_ID = "tao-shen/HuggingClaw-Home-data"
|
| 520 |
CHATLOG_PATH = "conversation-log/chatlog.jsonl"
|
| 521 |
+
_chatlog_buffer = []
|
| 522 |
+
CHATLOG_FLUSH_INTERVAL = 3
|
| 523 |
|
| 524 |
+
def persist_turn(speaker, turn_num, text_en, text_zh, actions, wf_state, child_stage):
|
|
|
|
| 525 |
import datetime
|
| 526 |
record = {
|
| 527 |
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
|
|
|
| 530 |
"text_en": text_en,
|
| 531 |
"text_zh": text_zh,
|
| 532 |
"actions": [{"action": a["action"], "result": a["result"][:500]} for a in actions],
|
| 533 |
+
"workflow_state": wf_state,
|
| 534 |
"child_stage": child_stage,
|
| 535 |
}
|
| 536 |
_chatlog_buffer.append(json.dumps(record, ensure_ascii=False))
|
|
|
|
|
|
|
| 537 |
try:
|
| 538 |
with open("/tmp/conversation-loop-full.jsonl", "a") as f:
|
| 539 |
f.write(_chatlog_buffer[-1] + "\n")
|
| 540 |
except:
|
| 541 |
pass
|
|
|
|
|
|
|
| 542 |
if len(_chatlog_buffer) >= CHATLOG_FLUSH_INTERVAL:
|
| 543 |
flush_chatlog()
|
| 544 |
|
| 545 |
|
| 546 |
def flush_chatlog():
|
|
|
|
| 547 |
global _chatlog_buffer
|
| 548 |
if not _chatlog_buffer:
|
| 549 |
return
|
| 550 |
batch = "\n".join(_chatlog_buffer) + "\n"
|
| 551 |
_chatlog_buffer = []
|
| 552 |
try:
|
|
|
|
| 553 |
existing = ""
|
| 554 |
try:
|
| 555 |
dl = hf_hub_download(HOME_DATASET_ID, CHATLOG_PATH,
|
|
|
|
| 557 |
with open(dl) as f:
|
| 558 |
existing = f.read()
|
| 559 |
except:
|
| 560 |
+
pass
|
|
|
|
| 561 |
hf_api.upload_file(
|
| 562 |
+
path_or_fileobj=io.BytesIO((existing + batch).encode()),
|
| 563 |
path_in_repo=CHATLOG_PATH,
|
| 564 |
repo_id=HOME_DATASET_ID, repo_type="dataset",
|
| 565 |
)
|
| 566 |
+
print(f"[PERSIST] Flushed {batch.count(chr(10))} turn(s)")
|
| 567 |
except Exception as e:
|
|
|
|
| 568 |
_chatlog_buffer = batch.strip().split("\n") + _chatlog_buffer
|
| 569 |
print(f"[PERSIST] Flush failed: {e}")
|
| 570 |
|
|
|
|
| 578 |
|
| 579 |
|
| 580 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 581 |
+
# MODULE 5: TURN EXECUTION — Parse [TASK] and route to Claude Code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 583 |
|
| 584 |
history = []
|
| 585 |
MAX_HISTORY = 24
|
| 586 |
last_action_results = []
|
|
|
|
| 587 |
turn_count = 0
|
| 588 |
+
last_claude_code_result = ""
|
| 589 |
|
| 590 |
+
# Simple workflow state: BIRTH / WAITING / ACTIVE
|
| 591 |
+
workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
|
| 593 |
|
| 594 |
+
def parse_and_execute_turn(raw_text, ctx):
|
| 595 |
+
"""Parse LLM output. Route [TASK] to Claude Code, handle few escape-hatch actions."""
|
| 596 |
+
global _pending_cooldown, last_rebuild_trigger_at, last_claude_code_result
|
| 597 |
+
results = []
|
| 598 |
+
|
| 599 |
+
# 1. Handle create_child (BIRTH state only)
|
| 600 |
+
if "[ACTION: create_child]" in raw_text or "[ACTION:create_child]" in raw_text:
|
| 601 |
+
result = action_create_child()
|
| 602 |
+
results.append({"action": "create_child", "result": result})
|
| 603 |
+
return raw_text, results
|
| 604 |
+
|
| 605 |
+
# 2. Handle [TASK]...[/TASK] → Claude Code
|
| 606 |
+
task_match = re.search(r'\[TASK\](.*?)\[/TASK\]', raw_text, re.DOTALL)
|
| 607 |
+
if task_match:
|
| 608 |
+
task_desc = task_match.group(1).strip()
|
| 609 |
+
if not task_desc:
|
| 610 |
+
results.append({"action": "task", "result": "Empty task description."})
|
| 611 |
+
elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 612 |
+
results.append({"action": "task", "result": f"BLOCKED: Cain is {child_state['stage']}. Wait for it to finish."})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
else:
|
| 614 |
+
# Check cooldown
|
| 615 |
+
check_and_clear_cooldown()
|
| 616 |
+
if last_rebuild_trigger_at > 0:
|
| 617 |
+
elapsed = time.time() - last_rebuild_trigger_at
|
| 618 |
+
if elapsed < REBUILD_COOLDOWN_SECS:
|
| 619 |
+
results.append({"action": "task", "result": f"BLOCKED: Cooldown ({int(REBUILD_COOLDOWN_SECS - elapsed)}s remaining). Cain is still building from your last change."})
|
| 620 |
+
else:
|
| 621 |
+
last_rebuild_trigger_at = 0
|
| 622 |
+
|
| 623 |
+
if not results: # not blocked
|
| 624 |
+
enriched = enrich_task_with_context(task_desc, ctx)
|
| 625 |
+
print(f"[TASK] Sending to Claude Code ({len(enriched)} chars)...")
|
| 626 |
+
result = action_claude_code(enriched)
|
| 627 |
+
results.append({"action": "claude_code", "result": result})
|
| 628 |
+
last_claude_code_result = result
|
| 629 |
+
# Clear context cache since files may have changed
|
| 630 |
+
_context_cache.clear()
|
| 631 |
+
|
| 632 |
+
# 3. Handle [ACTION: restart] (escape hatch)
|
| 633 |
+
if re.search(r'\[ACTION:\s*restart\]', raw_text):
|
| 634 |
+
result = action_restart()
|
| 635 |
+
results.append({"action": "restart", "result": result})
|
| 636 |
+
|
| 637 |
+
# 4. Handle [ACTION: send_bubble:...] (parent-child communication)
|
| 638 |
+
bubble_match = re.search(r'\[ACTION:\s*send_bubble:([^\]]+)\]', raw_text)
|
| 639 |
+
if bubble_match:
|
| 640 |
+
result = action_send_bubble(bubble_match.group(1).strip())
|
| 641 |
+
results.append({"action": "send_bubble", "result": result})
|
| 642 |
+
|
| 643 |
+
# Activate deferred cooldown
|
| 644 |
+
if _pending_cooldown:
|
| 645 |
+
last_rebuild_trigger_at = time.time()
|
| 646 |
+
_pending_cooldown = False
|
| 647 |
+
print(f"[COOLDOWN] Rebuild cooldown activated ({REBUILD_COOLDOWN_SECS}s)")
|
| 648 |
|
| 649 |
+
# Clean text for display
|
| 650 |
+
clean = re.sub(r'\[TASK\].*?\[/TASK\]', '', raw_text, flags=re.DOTALL)
|
| 651 |
+
clean = re.sub(r'\[ACTION:[^\]]*\]', '', clean).strip()
|
| 652 |
+
|
| 653 |
+
return clean, results
|
| 654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 657 |
+
# MODULE 6: PROMPT BUILDING
|
| 658 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 659 |
|
| 660 |
def build_system_prompt():
|
| 661 |
+
"""System prompt: tell agents they're project managers for Claude Code."""
|
| 662 |
|
|
|
|
| 663 |
if not child_state["created"]:
|
| 664 |
+
return f"""You are a POWERFUL AI agent on HuggingFace Spaces. You and your partner are parents who need to create your child {CHILD_NAME}.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
|
| 666 |
+
Use [ACTION: create_child] to birth {CHILD_NAME} as a new HuggingFace Space.
|
|
|
|
| 667 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
English first, then --- separator, then Chinese translation."""
|
| 669 |
|
| 670 |
+
return f"""You are a POWERFUL AI agent on HuggingFace Spaces. You and your partner are parents of {CHILD_NAME}.
|
| 671 |
+
|
| 672 |
+
YOUR ROLE: You are a **project manager**, NOT a coder. Your job is to:
|
| 673 |
+
1. Understand {CHILD_NAME}'s current situation from the context provided
|
| 674 |
+
2. Discuss with your partner (2-3 sentences) what needs to be done
|
| 675 |
+
3. Write a clear, specific [TASK]...[/TASK] block that tells Claude Code what to fix
|
| 676 |
+
|
| 677 |
+
Claude Code is your engineer — it will clone {CHILD_NAME}'s code, analyze it, fix issues, and push changes.
|
| 678 |
+
You do NOT read files or write code yourself. You analyze the situation and give Claude Code clear instructions.
|
| 679 |
+
|
| 680 |
+
IMPORTANT KNOWLEDGE — HuggingFace Spaces CONFIG_ERROR:
|
| 681 |
+
- "Collision on variables and secrets names" means a HF Space has an ENVIRONMENT VARIABLE and a SECRET with the SAME NAME.
|
| 682 |
+
- This is NOT about duplicate JSON keys. It's about the HF Space settings page.
|
| 683 |
+
- Fix: use [ACTION: restart] after identifying which env var names collide with secret names.
|
| 684 |
+
- The env vars and secrets are shown in the context below. Look for names that appear in BOTH the Variables and Secrets lists.
|
| 685 |
+
- To fix: the colliding variable or secret needs to be removed. Claude Code cannot do this — use the discussion to identify the collision, then tell your partner.
|
| 686 |
+
- Actually, this may require manual intervention via HF API. Discuss what you find.
|
| 687 |
+
|
| 688 |
+
AVAILABLE ACTIONS:
|
| 689 |
+
[TASK]
|
| 690 |
+
Detailed task description for Claude Code...
|
| 691 |
+
Include: what's wrong, which files to look at, what the fix should be.
|
| 692 |
+
[/TASK]
|
| 693 |
+
|
| 694 |
+
[ACTION: restart] — Restart {CHILD_NAME}'s Space
|
| 695 |
+
[ACTION: send_bubble:MESSAGE] — Send a message to {CHILD_NAME}
|
| 696 |
+
[ACTION: create_child] — Create {CHILD_NAME} (if not born)
|
| 697 |
+
|
| 698 |
+
HF SPACES TECHNICAL NOTES:
|
| 699 |
+
- Docker containers MUST bind port 7860. Without this = stuck in APP_STARTING forever.
|
| 700 |
+
- gradio MUST be in requirements.txt. NEVER remove it.
|
| 701 |
+
- OOM (exit 137) = reduce dependencies, NOT remove gradio.
|
| 702 |
+
- NEVER install torch/transformers unless required (2GB+, causes OOM on free tier).
|
| 703 |
+
- If sdk: gradio in README.md, Dockerfile is IGNORED. Use sdk: docker for Dockerfile control.
|
| 704 |
|
| 705 |
+
CONVERSATION RULES:
|
| 706 |
+
1. Brief dialogue (2-3 sentences) analyzing the situation
|
| 707 |
+
2. Then a [TASK]...[/TASK] block OR discussion about what to do
|
| 708 |
+
3. English first, then --- separator, then Chinese translation
|
| 709 |
+
4. Be SPECIFIC in task descriptions — include error messages, file names, expected behavior
|
| 710 |
+
5. If Cain is BUILDING/RESTARTING, just discuss — no [TASK] needed"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
+
def build_user_prompt(speaker, other, ctx):
|
| 714 |
+
"""Build the user prompt with context and conversation history."""
|
| 715 |
+
parts = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
+
# Conversation history
|
| 718 |
+
if history:
|
| 719 |
+
parts.append("=== RECENT CONVERSATION ===")
|
| 720 |
+
for h in history[-8:]:
|
| 721 |
+
parts.append(f"{h['speaker']}: {h['text'][:300]}")
|
| 722 |
|
| 723 |
+
# Last action results
|
| 724 |
+
if last_action_results:
|
| 725 |
+
parts.append("\n=== LAST ACTION RESULTS ===")
|
| 726 |
+
for ar in last_action_results:
|
| 727 |
+
parts.append(f"[{ar['action']}]: {ar['result'][:500]}")
|
| 728 |
+
|
| 729 |
+
# Last Claude Code result (if any)
|
| 730 |
+
if last_claude_code_result:
|
| 731 |
+
parts.append(f"\n=== LAST CLAUDE CODE RESULT ===\n{last_claude_code_result[:1500]}")
|
| 732 |
+
|
| 733 |
+
# Auto-gathered context
|
| 734 |
+
parts.append(f"\n=== {CHILD_NAME}'S CURRENT STATE (auto-gathered) ===")
|
| 735 |
+
parts.append(format_context(ctx))
|
| 736 |
+
|
| 737 |
+
# Guidance based on state
|
| 738 |
+
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 739 |
+
parts.append(f"\n⏳ {CHILD_NAME} is {child_state['stage']}. Just discuss what you'll check next. Do NOT write a [TASK].")
|
| 740 |
+
elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 741 |
+
parts.append(f"\n🚨 {CHILD_NAME} has {child_state['stage']}! Analyze the context above and write a [TASK] for Claude Code to fix it.")
|
| 742 |
+
parts.append("Be SPECIFIC: include the error message, relevant files, and what the fix should do.")
|
| 743 |
+
elif child_state["alive"]:
|
| 744 |
+
parts.append(f"\n✅ {CHILD_NAME} is alive! Write a [TASK] for Claude Code to improve {CHILD_NAME} (add features, harden survival, etc).")
|
| 745 |
else:
|
| 746 |
+
parts.append(f"\nAnalyze the situation and decide what to do.")
|
| 747 |
|
| 748 |
+
parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
|
| 749 |
+
parts.append("English first, then --- separator, then Chinese translation.")
|
| 750 |
+
|
| 751 |
+
return "\n".join(parts)
|
| 752 |
|
| 753 |
|
| 754 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 755 |
+
# MODULE 7: MAIN LOOP
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
# ══════════════════════════════════════════════════════════════════════════════
|
| 757 |
|
| 758 |
+
# Flush conversation log on exit
|
| 759 |
import atexit, signal
|
| 760 |
atexit.register(flush_chatlog)
|
| 761 |
def _signal_flush(signum, frame):
|
|
|
|
| 764 |
signal.signal(signal.SIGTERM, _signal_flush)
|
| 765 |
|
| 766 |
print("\n" + "="*60)
|
| 767 |
+
print(" Adam & Eve — Claude Code Orchestrators (v2)")
|
| 768 |
+
print(" Agents discuss → Claude Code executes")
|
| 769 |
print("="*60 + "\n")
|
| 770 |
|
| 771 |
post_chatlog([]) # Clear chatlog
|
| 772 |
|
| 773 |
+
# Opening turn
|
| 774 |
+
ctx = gather_context()
|
| 775 |
if child_state["created"]:
|
| 776 |
+
opening = (f"Your child {CHILD_NAME} exists (stage: {child_state['stage']}). "
|
| 777 |
+
f"Context has been auto-gathered. Analyze the situation and write a [TASK] for Claude Code if needed.")
|
|
|
|
|
|
|
|
|
|
| 778 |
else:
|
| 779 |
+
opening = f"You and Eve need to create your first child. Use [ACTION: create_child] to bring them to life."
|
| 780 |
+
|
| 781 |
+
reply = call_llm(build_system_prompt(), f"You are Adam. {opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
if reply:
|
| 783 |
+
clean, actions = parse_and_execute_turn(reply, ctx)
|
| 784 |
last_action_results = actions
|
| 785 |
en, zh = parse_bilingual(clean)
|
| 786 |
print(f"[Adam/EN] {en}")
|
| 787 |
if zh != en:
|
| 788 |
print(f"[Adam/ZH] {zh}")
|
| 789 |
+
for ar in actions:
|
| 790 |
+
print(f"[Adam/DID] {ar['action']}")
|
|
|
|
| 791 |
entry = {"speaker": "Adam", "text": en, "text_zh": zh}
|
| 792 |
if actions:
|
| 793 |
labels = " ".join(f"🔧{ar['action'].split(':')[0]}" for ar in actions)
|
|
|
|
| 800 |
|
| 801 |
time.sleep(20)
|
| 802 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
|
| 804 |
+
def do_turn(speaker, other, space_url):
|
| 805 |
+
"""Execute one conversation turn."""
|
| 806 |
+
global last_action_results, turn_count, last_claude_code_result
|
| 807 |
+
turn_count += 1
|
| 808 |
+
|
| 809 |
+
# Auto-gather context
|
| 810 |
+
ctx = gather_context()
|
| 811 |
+
|
| 812 |
+
system = build_system_prompt()
|
| 813 |
+
user = build_user_prompt(speaker, other, ctx)
|
| 814 |
+
t0 = time.time()
|
| 815 |
+
raw_reply = call_llm(system, user)
|
| 816 |
+
|
| 817 |
+
if not raw_reply:
|
| 818 |
+
print(f"[{speaker}] (no response)")
|
| 819 |
+
return False
|
| 820 |
+
|
| 821 |
+
clean_text, action_results = parse_and_execute_turn(raw_reply, ctx)
|
| 822 |
+
elapsed = time.time() - t0
|
| 823 |
+
last_action_results = action_results
|
| 824 |
+
|
| 825 |
+
en, zh = parse_bilingual(clean_text)
|
| 826 |
+
print(f"[{speaker}/EN] {en}")
|
| 827 |
+
if zh != en:
|
| 828 |
+
print(f"[{speaker}/ZH] {zh}")
|
| 829 |
+
if action_results:
|
| 830 |
+
for ar in action_results:
|
| 831 |
+
print(f"[{speaker}/DID] {ar['action']}")
|
| 832 |
+
print(f"[{speaker}] Turn #{turn_count}: {len(action_results)} action(s) in {elapsed:.1f}s")
|
| 833 |
+
else:
|
| 834 |
+
print(f"[{speaker}] Turn #{turn_count}: discussion only ({elapsed:.1f}s)")
|
| 835 |
+
|
| 836 |
+
# Add to history
|
| 837 |
+
if action_results:
|
| 838 |
+
labels = " ".join(f"🔧{ar['action'].split(':')[0]}" for ar in action_results)
|
| 839 |
+
history.append({"speaker": speaker, "text": f"{en} {labels}", "text_zh": f"{zh} {labels}"})
|
| 840 |
+
else:
|
| 841 |
+
history.append({"speaker": speaker, "text": en, "text_zh": zh})
|
| 842 |
+
|
| 843 |
+
set_bubble(space_url, en, zh)
|
| 844 |
+
post_chatlog(history)
|
| 845 |
+
persist_turn(speaker, turn_count, en, zh, action_results, workflow_state, child_state["stage"])
|
| 846 |
+
return True
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
# Main loop: Adam → Eve → Adam → Eve → ...
|
| 850 |
while True:
|
| 851 |
+
# Smart wait: if Cain is BUILDING/RESTARTING, skip Claude Code, just discuss
|
| 852 |
+
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
| 853 |
+
check_and_clear_cooldown()
|
| 854 |
+
try:
|
| 855 |
+
info = hf_api.space_info(CHILD_SPACE_ID)
|
| 856 |
+
new_stage = info.runtime.stage if info.runtime else "unknown"
|
| 857 |
+
if new_stage != child_state["stage"]:
|
| 858 |
+
print(f"[WAIT] Stage changed: {child_state['stage']} → {new_stage}")
|
| 859 |
+
child_state["stage"] = new_stage
|
| 860 |
+
child_state["alive"] = (new_stage == "RUNNING")
|
| 861 |
+
_context_cache.clear()
|
| 862 |
+
else:
|
| 863 |
+
print(f"[WAIT] Still {new_stage}... waiting 20s")
|
| 864 |
+
time.sleep(20)
|
| 865 |
+
continue
|
| 866 |
+
except Exception as e:
|
| 867 |
+
print(f"[WAIT] Health check error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
time.sleep(20)
|
| 869 |
continue
|
| 870 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 871 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 872 |
+
time.sleep(20)
|
| 873 |
|
| 874 |
+
# Skip Adam if Claude Code just pushed (Cain will be rebuilding)
|
| 875 |
+
if child_state["stage"] in ("BUILDING", "RESTARTING"):
|
| 876 |
+
print(f"[SKIP] Cain entered {child_state['stage']} — skipping Adam's turn")
|
| 877 |
time.sleep(10)
|
| 878 |
continue
|
| 879 |
|
|
|
|
| 882 |
if len(history) > MAX_HISTORY:
|
| 883 |
history = history[-MAX_HISTORY:]
|
| 884 |
|
| 885 |
+
time.sleep(20)
|