tao-shen Claude Opus 4.6 commited on
Commit
f3f1288
·
1 Parent(s): 5c981dd

refactor: conversation-loop v2 — Claude Code as primary executor

Browse files

Complete 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>

Files changed (1) hide show
  1. scripts/conversation-loop.py +337 -1081
scripts/conversation-loop.py CHANGED
@@ -1,72 +1,36 @@
1
  #!/usr/bin/env python3 -u
2
  """
3
- Adam & Eve — Autonomous Agents with FULL control over their child.
4
 
5
- They have complete access to their child (Cain) on HuggingFace:
6
- - Read/write ANY file in the Space repo (code, Dockerfile, scripts...)
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
- # ║ ┌─────────────┐ LLM API ┌────────────────┐ ║
20
- # ║ │ Zhipu GLM │ ◄───────────► │ CONVERSATION │ ║
21
- # ║ │ (glm-4.5) │ system + │ ENGINE │
22
- # ║ └─────────────┘ user prompt
23
- # ║ │ ┌────────────┐│
24
- # ║ │ State ││
25
- # ║ │ │ Machine ││
26
- # ║ ┌─────────────┐ │ │ BIRTH → ││
27
- # ║ │ ACTION │ ◄───parsed───── DIAGNOSE →
28
- # ║ │ PARSER │ [ACTION/操作] │ACT ││ ║
29
- # ║ │ + 🔧🛠️ emoji │ case-insens. VERIFY
30
- # ║ └────────────┘ │ │ MONITOR ││
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
- # Multi-action & sub-agent limits
141
- MAX_ACTIONS_PER_TURN = 5 # Allow up to 5 actions per turn (was 1)
142
- MAX_DELEGATE_DEPTH = 1 # Sub-agents cannot delegate further
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 (dynamic cooldown)."""
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: # always wait at least 60s
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 early ({int(elapsed)}s elapsed)")
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 — Full access to the child
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] {CHILD_NAME} created!")
235
- return (f"SUCCESS! {CHILD_NAME} born! Space: {CHILD_SPACE_ID}, "
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. Use [ACTION: create_child] first."
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
- files_written_this_cycle.clear() # reset write dedup on successful run
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
- # Also try to get container logs for more context
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 # deferred — activated after turn ends
357
- return f"{CHILD_NAME} is restarting. Will take a few minutes. Cooldown starts after this turn."
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 (appears as bubble text)."""
498
  try:
499
  requests.post(f"{CHILD_SPACE_URL}/api/bubble",
500
  json={"text": text, "text_zh": text}, timeout=5)
501
- return f"Sent message to {CHILD_NAME}: \"{text}\""
502
  except Exception as e:
503
- return f"Error sending message: {e}"
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. Use [ACTION: create_child] first."
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", # non-interactive mode
557
  })
558
 
559
- print(f"[CLAUDE-CODE] Running: {task[:100]}...")
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 to start: {e}"
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 # triggers rebuild cooldown
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, total {len(output)} chars)"
605
 
606
  return f"=== Claude Code Output ===\n{output}\n\n=== Changes ===\n{push_result}"
607
 
608
 
609
  # ══════════════════════════════════════════════════════════════════════════════
610
- # MODULE 2B: SUB-AGENT DELEGATION
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
- def execute_subtask(task_description, parent_speaker):
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
- result_text = "\n".join(summary_parts)
659
- print(f"[SUB-AGENT] Done: {task_description[:60]} ({len(actions)} actions)")
660
- return {"task": task_description, "result": result_text, "actions": actions}
661
 
 
 
662
 
663
- # ══════════════════════════════════════════════════════════════════════════════
664
- # MODULE 3: ACTION PARSER — Extract and execute actions from LLM output
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
- def parse_and_execute_actions(raw_text, depth=0):
672
- """Parse [ACTION: ...] from LLM output. Execute. Return (clean_text, results).
673
- Multi-action: up to MAX_ACTIONS_PER_TURN actions per turn.
674
- Delegate actions are collected and executed in parallel at the end."""
675
- global last_rebuild_trigger_at, _pending_cooldown
676
- results = []
677
- executed = set() # Deduplicate
678
- pending_delegates = [] # Collect delegate tasks for parallel execution
679
-
680
- # 1. Handle write_file with [CONTENT]...[/CONTENT] block
681
- # Tolerates: [ACTION/Action/操作: write_file:...], [write_file:...], missing prefix,
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
- # Deduplicate
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
- # 3. Fallback: parse emoji action format (🔧 🛠️ etc.) — LLM sometimes uses this
840
- if not results:
841
- for match in re.finditer(r'[🔧🛠️]\ufe0f?\s*(\w+(?::\S+)*)', raw_text):
842
- action_str = match.group(1).strip()
843
- if action_str in executed:
844
- continue
845
- executed.add(action_str)
846
- # Re-wrap as [ACTION: ...] format and recurse through same logic
847
- parts = [p.strip() for p in action_str.split(":")]
848
- name = parts[0]
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
- return clean, results
 
 
 
 
 
 
 
 
 
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). Handle action tags gracefully."""
1019
- # Remove action tags and content blocks for display
1020
- display = re.sub(r'\[ACTION:[^\]]*\]', '', text)
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
- # Find the split point: first paragraph with Chinese is the start of zh
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 = [] # Buffer entries, flush every N turns to avoid API spam
1068
- CHATLOG_FLUSH_INTERVAL = 3 # Flush every 3 turns
1069
 
1070
- def persist_turn(speaker, turn_num, text_en, text_zh, actions, workflow_state_str, child_stage):
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": workflow_state_str,
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 # File doesn't exist yet, start fresh
1114
- combined = existing + batch
1115
  hf_api.upload_file(
1116
- path_or_fileobj=io.BytesIO(combined.encode()),
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) to {HOME_DATASET_ID}/{CHATLOG_PATH}")
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: CONVERSATION ENGINEState Machine + Knowledge Tracking
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
- # ── Workflow State Machine ──
1162
- # States: BIRTH DIAGNOSE ACT → VERIFY → MONITOR → (DIAGNOSE if error)
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 transition_state(new_state):
1176
- """Transition to a new workflow state."""
1177
- global workflow_state, workflow_turns_in_state
1178
- if new_state != workflow_state:
1179
- print(f"[STATE] {workflow_state} → {new_state}")
1180
- workflow_state = new_state
1181
- workflow_turns_in_state = 0
1182
-
1183
-
1184
- def update_workflow_from_actions(action_results):
1185
- """Update state machine based on what just happened."""
1186
- global workflow_turns_in_state
1187
- workflow_turns_in_state += 1
1188
-
1189
- for ar in action_results:
1190
- action_name = ar["action"].split(":")[0]
1191
- action_key = ar["action"]
1192
-
1193
- # Track knowledge
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
- transition_state("DIAGNOSE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- status = get_child_status()
1255
 
1256
- actions_section = ""
1257
  if not child_state["created"]:
1258
- actions_section = """
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
- # State-machine-driven guidance
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
- def _get_guidance(speaker):
1399
- """State-machine-driven guidance clear, phase-appropriate directions."""
1400
- if workflow_state == "BIRTH":
1401
- return "Your child hasn't been born yet. Use [ACTION: create_child] NOW!"
1402
-
1403
- elif workflow_state == "DIAGNOSE":
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
- # Parse and execute actions (may include parallel sub-agent delegation)
1480
- clean_text, action_results = parse_and_execute_actions(raw_reply)
1481
- elapsed = time.time() - t0
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
- # Update workflow state machine
1488
- update_workflow_from_actions(action_results)
 
 
 
1489
 
1490
- # Parse bilingual
1491
- en, zh = parse_bilingual(clean_text)
1492
- print(f"[{speaker}/EN] {en}")
1493
- if zh != en:
1494
- print(f"[{speaker}/ZH] {zh}")
1495
- n_actions = len(action_results)
1496
- if action_results:
1497
- for ar in action_results:
1498
- print(f"[{speaker}/DID] {ar['action']}")
1499
- print(f"[{speaker}] Turn #{turn_count}: {n_actions} action(s) in {elapsed:.1f}s")
1500
-
1501
- # Add action summary to chat entry
1502
- if action_results:
1503
- action_labels = " ".join(f"🔧{ar['action'].split(':')[0]}" for ar in action_results)
1504
- history.append({"speaker": speaker, "text": f"{en} {action_labels}", "text_zh": f"{zh} {action_labels}"})
 
 
 
 
 
 
 
1505
  else:
1506
- history.append({"speaker": speaker, "text": en, "text_zh": zh})
1507
 
1508
- set_bubble(space_url, en, zh)
1509
- post_chatlog(history)
1510
- persist_turn(speaker, turn_count, en, zh, action_results, workflow_state, child_state["stage"])
1511
- return True
1512
 
1513
 
1514
  # ══════════════════════════════════════════════════════════════════════════════
1515
- # MODULE 6: MAIN LOOP
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 (SIGTERM from kill, or normal 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 — Multi-Action Agents (GLM-4.5)")
1533
- print(" Up to 5 actions/turn, sub-agent delegation, parallel work")
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} already exists (stage: {child_state['stage']}). "
1541
- f"You have FULL access to their code and data. "
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 = (f"You and Eve need to create your first child. "
1547
- f"You have the power to create a new HuggingFace Space. "
1548
- f"Discuss with Eve, then use [ACTION: create_child] to bring them to life.")
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 = parse_and_execute_actions(reply)
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
- if actions:
1563
- for ar in actions:
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/APP_STARTING, skip LLM calls and just poll
1584
- # But NOT during grace period after timeout — agents need consecutive turns to diagnose & fix
1585
- if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING") and grace_turns_remaining <= 0:
1586
- smart_wait_count += 1
1587
- if smart_wait_count > MAX_SMART_WAIT_POLLS:
1588
- print(f"[WAIT-TIMEOUT] {smart_wait_count} polls (~{smart_wait_count*20}s) on {child_state['stage']} resuming {GRACE_TURNS_AFTER_TIMEOUT} agent turn pairs to diagnose")
1589
- smart_wait_count = 0
1590
- grace_turns_remaining = GRACE_TURNS_AFTER_TIMEOUT
1591
- # Fall through to normal agent turns
1592
- else:
1593
- print(f"[WAIT] Cain is {child_state['stage']} — polling health instead of LLM call... ({smart_wait_count}/{MAX_SMART_WAIT_POLLS})")
1594
- check_and_clear_cooldown()
1595
- # Quick health check to update stage
1596
- try:
1597
- info = hf_api.space_info(CHILD_SPACE_ID)
1598
- new_stage = info.runtime.stage if info.runtime else "unknown"
1599
- if new_stage != child_state["stage"]:
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) # longer pause — each turn does more work now
1617
 
1618
- # Check if we just triggered a build skip Adam's turn ONLY if not in grace period
1619
- if child_state["stage"] in ("BUILDING", "RESTARTING") and grace_turns_remaining <= 0:
1620
- print(f"[SKIP] Cain entered {child_state['stage']} — skipping Adam's turn to avoid wasted LLM call")
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) # longer pause — each turn does more work now
 
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 EXECUTIONParse [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)