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

feat: fix page width overflow + add architecture comments to conversation-loop

Browse files

- Make stage-row respect viewport width (max-width: 98vw)
- Game container now responsive (width: 100%, max-width: 1280px, aspect-ratio: 16/9)
- Chatlog panel 300px with stretch height to match game
- Add system architecture diagram and module-level comments to conversation-loop.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

frontend/electron-standalone.html CHANGED
@@ -400,36 +400,45 @@
400
  body.desktop-shell #asset-drawer {
401
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45) !important;
402
  }
403
- /* 主舞台 + 右侧对话布局 */
404
  #stage-row {
405
  display: flex;
406
  flex-direction: row;
407
  align-items: flex-start;
408
  gap: 16px;
 
 
409
  }
410
  #main-stage {
411
  position: relative;
412
- width: 1280px;
413
- flex-shrink: 0;
 
414
  transition: margin-left .25s ease;
415
  will-change: margin-left;
416
  }
417
  body.drawer-open #main-stage {
418
  margin-left: 0 !important;
419
  }
 
 
 
 
420
  #bottom-panels {
421
  display: flex;
422
  gap: 20px;
423
- width: 1280px;
424
- max-width: none;
425
  justify-content: flex-start;
426
  margin-top: 20px;
427
  flex-wrap: wrap;
428
  }
429
  #chatlog-panel {
430
- width: 340px;
431
  flex-shrink: 0;
432
- height: 720px;
 
 
433
  background: #1a1d27;
434
  border: 4px solid #0e1119;
435
  padding: 12px 16px;
@@ -464,11 +473,9 @@
464
  position: relative;
465
  border: 0;
466
  image-rendering: pixelated;
467
- width: 1280px;
468
- height: 720px;
469
- max-width: none;
470
- max-height: none;
471
- aspect-ratio: auto;
472
  overflow: hidden;
473
  }
474
  #game-container canvas {
 
400
  body.desktop-shell #asset-drawer {
401
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45) !important;
402
  }
403
+ /* 主舞台 + 右侧对话布局 — 自适应页面宽度 */
404
  #stage-row {
405
  display: flex;
406
  flex-direction: row;
407
  align-items: flex-start;
408
  gap: 16px;
409
+ max-width: 98vw;
410
+ width: fit-content;
411
  }
412
  #main-stage {
413
  position: relative;
414
+ flex: 1 1 auto;
415
+ min-width: 0;
416
+ max-width: 1280px;
417
  transition: margin-left .25s ease;
418
  will-change: margin-left;
419
  }
420
  body.drawer-open #main-stage {
421
  margin-left: 0 !important;
422
  }
423
+ #game-container,
424
+ #game-container canvas {
425
+ max-width: 100% !important;
426
+ }
427
  #bottom-panels {
428
  display: flex;
429
  gap: 20px;
430
+ width: 100%;
431
+ max-width: 100%;
432
  justify-content: flex-start;
433
  margin-top: 20px;
434
  flex-wrap: wrap;
435
  }
436
  #chatlog-panel {
437
+ width: 300px;
438
  flex-shrink: 0;
439
+ min-height: 400px;
440
+ max-height: 720px;
441
+ align-self: stretch;
442
  background: #1a1d27;
443
  border: 4px solid #0e1119;
444
  padding: 12px 16px;
 
473
  position: relative;
474
  border: 0;
475
  image-rendering: pixelated;
476
+ width: 100%;
477
+ max-width: 1280px;
478
+ aspect-ratio: 16 / 9;
 
 
479
  overflow: hidden;
480
  }
481
  #game-container canvas {
scripts/conversation-loop.py CHANGED
@@ -11,6 +11,50 @@ They have complete access to their child (Cain) on HuggingFace:
11
  - Send messages to the child
12
 
13
  The LLM decides what to do. Actions use [ACTION: ...] tags.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
  import json, time, re, requests, sys, os, io
16
 
@@ -69,7 +113,10 @@ hf_api = HfApi(token=HF_TOKEN)
69
 
70
 
71
  # ══════════════════════════════════════════════════════════════════════════════
72
- # CHILD STATE
 
 
 
73
  # ══════════════════════════════════════════════════════════════════════════════
74
 
75
  child_state = {
@@ -105,7 +152,10 @@ init_child_state()
105
 
106
 
107
  # ══════════════════════════════════════════════════════════════════════════════
108
- # ACTIONS — Full access to the child
 
 
 
109
  # ══════════════════════════════════════════════════════════════════════════════
110
 
111
  def action_create_child():
@@ -171,8 +221,9 @@ def action_check_health():
171
  child_state["stage"] = stage
172
  child_state["alive"] = (stage == "RUNNING")
173
  if stage in ("RUNTIME_ERROR", "BUILD_ERROR"):
174
- # Get actual error message from runtime API
175
  error_detail = ""
 
176
  try:
177
  rresp = requests.get(
178
  f"https://huggingface.co/api/spaces/{CHILD_SPACE_ID}/runtime",
@@ -180,15 +231,39 @@ def action_check_health():
180
  if rresp.ok:
181
  rdata = rresp.json()
182
  error_detail = rdata.get("errorMessage", "")
183
- # Extract just the key error lines
184
  if error_detail:
185
  lines = [l.strip() for l in error_detail.split('\n') if l.strip() and '│' not in l]
186
- error_detail = " | ".join(lines[-5:]) # Last 5 meaningful lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  except:
188
  pass
189
  return (f"{CHILD_NAME} has a {stage}! "
190
  f"Error: {error_detail or 'unknown'}. "
191
- f"Options: [ACTION: restart] or fix the config with [ACTION: write_file:dataset:.openclaw/openclaw.json]")
 
 
192
  if stage in ("BUILDING", "STARTING", "APP_STARTING"):
193
  return f"{CHILD_NAME} is starting up (stage: {stage}). Be patient."
194
  return f"{CHILD_NAME} stage: {stage}. {'Running but API not responding.' if stage == 'RUNNING' else ''}"
@@ -316,7 +391,11 @@ def action_send_bubble(text):
316
 
317
 
318
  # ══════════════════════════════════════════════════════════════════════════════
319
- # ACTION PARSER — Extract and execute actions from LLM output
 
 
 
 
320
  # ══════════════════════════════════════════════════════════════════════════════
321
 
322
  def parse_and_execute_actions(raw_text):
@@ -360,6 +439,35 @@ def parse_and_execute_actions(raw_text):
360
  if len(results) >= 1:
361
  break
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  result = None
364
  if name == "create_child":
365
  result = action_create_child()
@@ -386,16 +494,82 @@ def parse_and_execute_actions(raw_text):
386
  results.append({"action": action_str, "result": result})
387
  print(f"[ACTION] {action_str} → {result[:120]}")
388
 
389
- # Clean the text: remove action tags and content blocks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  clean = re.sub(r'\[ACTION:[^\]]*\]', '', raw_text)
391
  clean = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', clean, flags=re.DOTALL)
 
392
  clean = clean.strip()
393
 
394
  return clean, results
395
 
396
 
397
  # ══════════════════════════════════════════════════════════════════════════════
398
- # LLM & COMMUNICATION
 
 
 
 
399
  # ══════════════════════════════════════════════════════════════════════════════
400
 
401
  def call_llm(system_prompt, user_prompt):
@@ -464,7 +638,23 @@ def set_bubble(url, text_en, text_zh=""):
464
 
465
 
466
  # ══════════════════════════════════════════════════════════════════════════════
467
- # CONVERSATION ENGINE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  # ══════════════════════════════════════════════════════════════════════════════
469
 
470
  history = []
@@ -473,6 +663,70 @@ last_action_results = []
473
  action_history = [] # Global log: [{"turn": N, "speaker": "Adam", "action": "...", "result": "..."}]
474
  turn_count = 0
475
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  def get_child_status():
478
  if not child_state["created"]:
@@ -482,6 +736,20 @@ def get_child_status():
482
  return f"Cain exists but status: {child_state['stage']}"
483
 
484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  def build_system_prompt():
486
  status = get_child_status()
487
 
@@ -496,7 +764,7 @@ ACTIONS — You can create your child:
496
  FULL ACCESS TO {CHILD_NAME} — You have COMPLETE control over your child.
497
  You can view and modify ANYTHING: code, config, memory, environment, everything.
498
 
499
- VIEWING (read-only, safe):
500
  [ACTION: check_health] — Is Cain alive? What's their status?
501
  [ACTION: list_files:space] — List ALL files in Cain's code repository
502
  [ACTION: list_files:dataset] — List ALL files in Cain's memory/data
@@ -504,7 +772,7 @@ VIEWING (read-only, safe):
504
  [ACTION: read_file:dataset:PATH] — Read any data/memory file
505
  [ACTION: get_env] — List Cain's environment variables
506
 
507
- MODIFYING (these change Cain — be thoughtful):
508
  [ACTION: write_file:space:PATH] — Write/update any code file
509
  [CONTENT] (triggers Space rebuild)
510
  file content here
@@ -523,22 +791,19 @@ MODIFYING (these change Cain — be thoughtful):
523
 
524
  return f"""You are an AI agent living on HuggingFace Spaces. You and your partner are parents raising your child {CHILD_NAME}.
525
 
526
- You have REAL execution power on HuggingFace. You can read and modify any file in your child's code and data. Your decisions lead to real changes.
527
 
528
  CHILD STATUS: {status}
529
  {actions_section}
530
  CONVERSATION RULES:
531
- 1. Output your spoken words — no "Adam:" or "Eve:" prefix
532
- 2. 2-4 sentences of dialogue, then optionally an action
533
  3. English first, then "---" on a new line, then Chinese translation
534
  4. Actions go AFTER your dialogue, before the --- separator
535
- 5. Use at most ONE action per turn
536
- 6. READ before you WRITEunderstand what's there first
537
- 7. Discuss with your partner before making big changes
538
- 8. Be a responsible parent — check on Cain, fix problems, help them grow
539
-
540
- WORKFLOW: First explore (list_files, read_file) → then understand → then improve (write_file) → then verify (check_health)
541
- Don't just talk about improving Cain — actually DO it. Read their code, find what to improve, write the improvement."""
542
 
543
 
544
  def build_user_prompt(speaker, other):
@@ -551,59 +816,77 @@ def build_user_prompt(speaker, other):
551
  for ar in last_action_results:
552
  action_context += f" [{ar['action']}]:\n{ar['result']}\n"
553
 
554
- # Guidance based on global action history prevent loops, push toward progress
555
- guidance = ""
556
- recent_actions = [ar["action"].split(":")[0] for ar in last_action_results] if last_action_results else []
557
 
558
- # Count action types in last 6 actions globally
559
- recent_global = action_history[-6:] if action_history else []
560
- global_action_names = [a["action"].split(":")[0] for a in recent_global]
561
- read_count = global_action_names.count("read_file")
562
- check_count = global_action_names.count("check_health")
563
- list_count = global_action_names.count("list_files")
564
- write_count = global_action_names.count("write_file")
565
-
566
- if not child_state["created"]:
567
- guidance = "Your child hasn't been born yet. Use [ACTION: create_child] now!"
568
- elif check_count + list_count >= 3 and write_count == 0 and read_count == 0:
569
- guidance = ("STOP checking health and listing files repeatedly! "
570
- "READ a specific file: [ACTION: read_file:space:Dockerfile] or "
571
- "[ACTION: read_file:dataset:.openclaw/openclaw.json]")
572
- elif read_count >= 3 and write_count == 0:
573
- guidance = ("You've read enough files. It's time to ACT! "
574
- "DECIDE what to change and use [ACTION: write_file:...] to make an improvement, "
575
- "or use [ACTION: restart] to restart Cain. Stop reading and START improving!")
576
- elif "write_file" in recent_actions:
577
- guidance = ("You just modified a file. Good! Now verify: "
578
- "use [ACTION: check_health] to see if Cain is recovering, "
579
- "or [ACTION: restart] to apply changes.")
580
- elif "restart" in recent_actions:
581
- guidance = ("You restarted Cain. Wait a moment, then [ACTION: check_health] to see the result.")
582
- elif "check_health" in recent_actions and child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR"):
583
- guidance = ("Cain has an error! Read the config [ACTION: read_file:dataset:.openclaw/openclaw.json] "
584
- "or try [ACTION: restart]. Don't just check_health again — take action to fix it!")
585
- elif "check_health" in recent_actions and child_state["alive"]:
586
- guidance = ("Cain is healthy! Think about improvements: "
587
- "read a file to understand it, then write an improved version. "
588
- "Or [ACTION: send_bubble:Hello Cain!] to communicate with your child.")
589
- elif "read_file" in recent_actions:
590
- guidance = ("You've read a file. Now DECIDE what to change and use "
591
- "[ACTION: write_file:space:PATH] or [ACTION: write_file:dataset:PATH] to improve it. "
592
- "Or discuss with your partner what you learned.")
593
- else:
594
- guidance = ("Explore your child: [ACTION: read_file:space:Dockerfile] to see the build, "
595
- "or [ACTION: read_file:dataset:.openclaw/openclaw.json] for config.")
596
 
597
  return f"""You are {speaker}, talking with {other}.
598
 
599
  Recent conversation:
600
  {conv_text}
601
  {action_context}
 
 
 
602
  Guidance: {guidance}
603
 
604
- Respond to {other}. Push forward don't just discuss, take action when appropriate.
605
- English first, then --- separator, then Chinese translation.
606
- If you take an action, put [ACTION: ...] after your dialogue, before the --- separator."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
608
 
609
  def do_turn(speaker, other, space_url):
@@ -626,6 +909,9 @@ def do_turn(speaker, other, space_url):
626
  action_history.append({"turn": turn_count, "speaker": speaker,
627
  "action": ar["action"], "result": ar["result"][:200]})
628
 
 
 
 
629
  # Parse bilingual
630
  en, zh = parse_bilingual(clean_text)
631
  print(f"[{speaker}/EN] {en}")
@@ -648,7 +934,11 @@ def do_turn(speaker, other, space_url):
648
 
649
 
650
  # ══════════════════════════════════════════════════════════════════════════════
651
- # MAIN LOOP
 
 
 
 
652
  # ══════════════════════════════════════════════════════════════════════════════
653
 
654
  print("\n" + "="*60)
 
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 │ or 🔧emoji │ │ 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
+ # ║ SAFETY LAYERS: ║
51
+ # ║ 1. Building-state guard: block write/restart during BUILDING ║
52
+ # ║ 2. ACT-phase guard: block reads when should be writing ║
53
+ # ║ 3. Knowledge dedup: block re-reading already-read files ║
54
+ # ║ 4. Config sanitizer: strip invalid openclaw.json keys ║
55
+ # ║ 5. Forced transitions: prevent infinite DIAGNOSE/VERIFY loops ║
56
+ # ║ ║
57
+ # ╚══════════════════════════════════════════════════════════════════════╝
58
  """
59
  import json, time, re, requests, sys, os, io
60
 
 
113
 
114
 
115
  # ══════════════════════════════════════════════════════════════════════════════
116
+ # MODULE 1: CHILD STATE
117
+ # Tracks Cain's current lifecycle: created? alive? stage? state?
118
+ # Updated by action_check_health(), action_restart(), etc.
119
+ # Used by state machine to decide transitions and by action parser for guards.
120
  # ══════════════════════════════════════════════════════════════════════════════
121
 
122
  child_state = {
 
152
 
153
 
154
  # ══════════════════════════════════════════════════════════════════════════════
155
+ # MODULE 2: ACTIONS — Full access to the child
156
+ # Each action_*() function maps to one [ACTION: ...] tag the LLM can emit.
157
+ # Actions modify Cain's Space/Dataset via HuggingFace Hub API.
158
+ # Results are fed back to the LLM in the next turn's prompt.
159
  # ══════════════════════════════════════════════════════════════════════════════
160
 
161
  def action_create_child():
 
221
  child_state["stage"] = stage
222
  child_state["alive"] = (stage == "RUNNING")
223
  if stage in ("RUNTIME_ERROR", "BUILD_ERROR"):
224
+ # Get error from runtime API + build logs for better diagnostics
225
  error_detail = ""
226
+ build_log_snippet = ""
227
  try:
228
  rresp = requests.get(
229
  f"https://huggingface.co/api/spaces/{CHILD_SPACE_ID}/runtime",
 
231
  if rresp.ok:
232
  rdata = rresp.json()
233
  error_detail = rdata.get("errorMessage", "")
 
234
  if error_detail:
235
  lines = [l.strip() for l in error_detail.split('\n') if l.strip() and '│' not in l]
236
+ error_detail = " | ".join(lines[-5:])
237
+ except:
238
+ pass
239
+ # Also try to get container logs for more context
240
+ try:
241
+ log_resp = requests.get(
242
+ f"https://api.hf.space/v1/{CHILD_SPACE_ID}/logs/run",
243
+ headers={"Authorization": f"Bearer {HF_TOKEN}"}, timeout=10,
244
+ stream=True)
245
+ if log_resp.ok:
246
+ log_lines = []
247
+ for line in log_resp.iter_lines(decode_unicode=True):
248
+ if line and line.startswith("data:"):
249
+ try:
250
+ entry = json.loads(line[5:])
251
+ log_lines.append(entry.get("data", "").strip())
252
+ except:
253
+ pass
254
+ if len(log_lines) >= 30:
255
+ break
256
+ # Get last meaningful log lines (skip empty, focus on errors)
257
+ meaningful = [l for l in log_lines if l and len(l) > 5]
258
+ if meaningful:
259
+ build_log_snippet = "\nRECENT LOGS:\n" + "\n".join(meaningful[-10:])
260
  except:
261
  pass
262
  return (f"{CHILD_NAME} has a {stage}! "
263
  f"Error: {error_detail or 'unknown'}. "
264
+ f"{build_log_snippet}"
265
+ f"\nOptions: [ACTION: restart] or fix code with [ACTION: write_file:space:PATH] "
266
+ f"or config with [ACTION: write_file:dataset:.openclaw/openclaw.json]")
267
  if stage in ("BUILDING", "STARTING", "APP_STARTING"):
268
  return f"{CHILD_NAME} is starting up (stage: {stage}). Be patient."
269
  return f"{CHILD_NAME} stage: {stage}. {'Running but API not responding.' if stage == 'RUNNING' else ''}"
 
391
 
392
 
393
  # ══════════════════════════════════════════════════════════════════════════════
394
+ # MODULE 3: ACTION PARSER — Extract and execute actions from LLM output
395
+ # Parse order: 1) [ACTION: write_file] with [CONTENT] block
396
+ # 2) [ACTION: ...] standard tags (one per turn)
397
+ # 3) 🔧emoji format fallback (LLM sometimes uses this)
398
+ # Safety guards applied: building-state, ACT-phase, knowledge dedup.
399
  # ══════════════════════════════════════════════════════════════════════════════
400
 
401
  def parse_and_execute_actions(raw_text):
 
439
  if len(results) >= 1:
440
  break
441
 
442
+ # Block restart/write when Cain is building — just wait
443
+ if child_state["stage"] in ("BUILDING", "RESTARTING") and name in ("restart", "write_file", "set_env", "set_secret"):
444
+ result = (f"⛔ BLOCKED: Cain is currently {child_state['stage']}. "
445
+ "Do NOT restart or make changes — wait for the build to finish. "
446
+ "Use [ACTION: check_health] to monitor progress.")
447
+ results.append({"action": action_str, "result": result})
448
+ print(f"[BLOCKED] {name} — Cain is {child_state['stage']}")
449
+ break
450
+
451
+ # Block read-only actions based on workflow state
452
+ if workflow_state == "ACT" and name in ("read_file", "list_files", "check_health"):
453
+ result = (f"⛔ BLOCKED: You are in ACTION phase. "
454
+ "You MUST use write_file, set_env, set_secret, or restart. "
455
+ "You already have enough information — make a change NOW.")
456
+ results.append({"action": action_str, "result": result})
457
+ print(f"[BLOCKED] {name} — forced ACT phase")
458
+ break
459
+
460
+ # Block re-reading files already in knowledge base
461
+ if name == "read_file" and len(args) >= 2:
462
+ file_key = ":".join(args)
463
+ if file_key in knowledge["files_read"]:
464
+ result = (f"⛔ You already read {file_key}. Use the information you have. "
465
+ "If you need to change it, use [ACTION: write_file:...]. "
466
+ "If you need a different file, read a NEW one.")
467
+ results.append({"action": action_str, "result": result})
468
+ print(f"[BLOCKED] {name} — already read {file_key}")
469
+ break
470
+
471
  result = None
472
  if name == "create_child":
473
  result = action_create_child()
 
494
  results.append({"action": action_str, "result": result})
495
  print(f"[ACTION] {action_str} → {result[:120]}")
496
 
497
+ # 3. Fallback: parse 🔧action:arg1:arg2 emoji format (LLM sometimes uses this)
498
+ if not results:
499
+ for match in re.finditer(r'🔧\s*(\w+(?::\S+)*)', raw_text):
500
+ action_str = match.group(1).strip()
501
+ if action_str in executed:
502
+ continue
503
+ executed.add(action_str)
504
+ # Re-wrap as [ACTION: ...] format and recurse through same logic
505
+ parts = [p.strip() for p in action_str.split(":")]
506
+ name = parts[0]
507
+ args = parts[1:]
508
+
509
+ if len(results) >= 1:
510
+ break
511
+
512
+ # Apply same blocking rules
513
+ if child_state["stage"] in ("BUILDING", "RESTARTING") and name in ("restart", "write_file", "set_env", "set_secret"):
514
+ result = (f"⛔ BLOCKED: Cain is currently {child_state['stage']}. Wait for it to finish.")
515
+ results.append({"action": action_str, "result": result})
516
+ print(f"[BLOCKED-emoji] {name} — Cain is {child_state['stage']}")
517
+ break
518
+
519
+ if workflow_state == "ACT" and name in ("read_file", "list_files", "check_health"):
520
+ result = (f"⛔ BLOCKED: You are in ACTION phase. "
521
+ "You MUST use write_file, set_env, set_secret, or restart.")
522
+ results.append({"action": action_str, "result": result})
523
+ print(f"[BLOCKED-emoji] {name} — forced ACT phase")
524
+ break
525
+
526
+ if name == "read_file" and len(args) >= 2:
527
+ file_key = ":".join(args)
528
+ if file_key in knowledge["files_read"]:
529
+ result = (f"⛔ You already read {file_key}. Use the information you have.")
530
+ results.append({"action": action_str, "result": result})
531
+ print(f"[BLOCKED-emoji] {name} — already read {file_key}")
532
+ break
533
+
534
+ result = None
535
+ if name == "create_child":
536
+ result = action_create_child()
537
+ elif name == "check_health":
538
+ result = action_check_health()
539
+ elif name == "restart":
540
+ result = action_restart()
541
+ elif name == "list_files" and len(args) >= 1:
542
+ result = action_list_files(args[0])
543
+ elif name == "read_file" and len(args) >= 2:
544
+ result = action_read_file(args[0], ":".join(args[1:]))
545
+ elif name == "set_env" and len(args) >= 2:
546
+ result = action_set_env(args[0], ":".join(args[1:]))
547
+ elif name == "set_secret" and len(args) >= 2:
548
+ result = action_set_secret(args[0], ":".join(args[1:]))
549
+ elif name == "get_env":
550
+ result = action_get_env()
551
+ elif name == "send_bubble" and len(args) >= 1:
552
+ result = action_send_bubble(":".join(args))
553
+
554
+ if result:
555
+ results.append({"action": action_str, "result": result})
556
+ print(f"[ACTION-emoji] {action_str} → {result[:120]}")
557
+
558
+ # Clean the text: remove action tags, content blocks, and emoji actions
559
  clean = re.sub(r'\[ACTION:[^\]]*\]', '', raw_text)
560
  clean = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', clean, flags=re.DOTALL)
561
+ clean = re.sub(r'🔧\s*\w+(?::\S+)*', '', clean)
562
  clean = clean.strip()
563
 
564
  return clean, results
565
 
566
 
567
  # ══════════════════════════════════════════════════════════════════════════════
568
+ # MODULE 4: LLM & COMMUNICATION
569
+ # call_llm(): Zhipu GLM via Anthropic-compatible API
570
+ # parse_bilingual(): Split "English --- Chinese" response
571
+ # post_chatlog(): Send conversation to Home Space for frontend display
572
+ # set_bubble(): Set bubble text on Adam/Eve Space pixel characters
573
  # ══════════════════════════════════════════════════════════════════════════════
574
 
575
  def call_llm(system_prompt, user_prompt):
 
638
 
639
 
640
  # ══════════════════════════════════════════════════════════════════════════════
641
+ # MODULE 5: CONVERSATION ENGINE — State Machine + Knowledge Tracking
642
+ # Core orchestration: manages turn-taking, state transitions, prompt building.
643
+ #
644
+ # State Machine: BIRTH → DIAGNOSE → ACT → VERIFY → MONITOR → (loop back)
645
+ # - BIRTH: Cain not yet created → force create_child
646
+ # - DIAGNOSE: Read files, check_health, gather information
647
+ # - ACT: Force write_file/set_env — stop reading, start fixing
648
+ # - VERIFY: check_health after changes, wait during BUILDING
649
+ # - MONITOR: Cain alive — explore, improve, communicate
650
+ #
651
+ # Knowledge Base: Tracks files_read/written/errors to prevent loops.
652
+ # Forced transitions: DIAGNOSE stuck ≥6 turns → ACT, VERIFY ≥4 → back.
653
+ #
654
+ # Prompt Builder:
655
+ # build_system_prompt(): Agent identity + available actions + rules
656
+ # build_user_prompt(): Conversation context + action results + guidance
657
+ # _get_guidance(): Phase-appropriate direction based on state machine
658
  # ══════════════════════════════════════════════════════════════════════════════
659
 
660
  history = []
 
663
  action_history = [] # Global log: [{"turn": N, "speaker": "Adam", "action": "...", "result": "..."}]
664
  turn_count = 0
665
 
666
+ # ── Workflow State Machine ──
667
+ # States: BIRTH → DIAGNOSE → ACT → VERIFY → MONITOR → (DIAGNOSE if error)
668
+ workflow_state = "BIRTH" if not child_state["created"] else "DIAGNOSE"
669
+ workflow_turns_in_state = 0 # How many turns spent in current state
670
+
671
+ # ── Knowledge Base — what has already been read/learned ──
672
+ knowledge = {
673
+ "files_read": set(), # "space:Dockerfile", "dataset:.openclaw/openclaw.json", etc.
674
+ "files_written": set(), # Files that have been modified
675
+ "errors_seen": [], # Error messages from check_health
676
+ "current_goal": "", # What are we trying to accomplish right now
677
+ }
678
+
679
+
680
+ def transition_state(new_state):
681
+ """Transition to a new workflow state."""
682
+ global workflow_state, workflow_turns_in_state
683
+ if new_state != workflow_state:
684
+ print(f"[STATE] {workflow_state} → {new_state}")
685
+ workflow_state = new_state
686
+ workflow_turns_in_state = 0
687
+
688
+
689
+ def update_workflow_from_actions(action_results):
690
+ """Update state machine based on what just happened."""
691
+ global workflow_turns_in_state
692
+ workflow_turns_in_state += 1
693
+
694
+ for ar in action_results:
695
+ action_name = ar["action"].split(":")[0]
696
+ action_key = ar["action"]
697
+
698
+ # Track knowledge
699
+ if action_name == "read_file":
700
+ knowledge["files_read"].add(":".join(ar["action"].split(":")[1:]))
701
+ elif action_name == "write_file":
702
+ knowledge["files_written"].add(":".join(ar["action"].split(":")[1:]))
703
+ elif action_name == "check_health":
704
+ if "ERROR" in ar.get("result", ""):
705
+ knowledge["errors_seen"].append(ar["result"][:200])
706
+
707
+ # State transitions
708
+ if action_name == "create_child":
709
+ transition_state("DIAGNOSE")
710
+ elif action_name in ("write_file", "set_env", "set_secret"):
711
+ transition_state("VERIFY")
712
+ elif action_name == "restart":
713
+ transition_state("VERIFY")
714
+ elif action_name == "check_health" and child_state["alive"]:
715
+ transition_state("MONITOR")
716
+ elif action_name == "check_health" and child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR"):
717
+ if workflow_state == "VERIFY":
718
+ transition_state("DIAGNOSE") # Fix didn't work, back to diagnosing
719
+
720
+ # Force transitions when stuck too long
721
+ if workflow_turns_in_state >= 6 and workflow_state == "DIAGNOSE":
722
+ transition_state("ACT")
723
+ print(f"[STATE] Forced to ACT — stuck in DIAGNOSE for {workflow_turns_in_state} turns")
724
+ elif workflow_turns_in_state >= 4 and workflow_state == "VERIFY":
725
+ if child_state["alive"]:
726
+ transition_state("MONITOR")
727
+ else:
728
+ transition_state("DIAGNOSE")
729
+
730
 
731
  def get_child_status():
732
  if not child_state["created"]:
 
736
  return f"Cain exists but status: {child_state['stage']}"
737
 
738
 
739
+ def get_knowledge_summary():
740
+ """Summarize what we already know — prevents redundant reads."""
741
+ lines = []
742
+ if knowledge["files_read"]:
743
+ lines.append("FILES ALREADY READ (do NOT re-read these): " + ", ".join(sorted(knowledge["files_read"])))
744
+ if knowledge["files_written"]:
745
+ lines.append("FILES ALREADY MODIFIED: " + ", ".join(sorted(knowledge["files_written"])))
746
+ if knowledge["errors_seen"]:
747
+ lines.append("KNOWN ERRORS: " + knowledge["errors_seen"][-1])
748
+ if knowledge["current_goal"]:
749
+ lines.append(f"CURRENT GOAL: {knowledge['current_goal']}")
750
+ return "\n".join(lines)
751
+
752
+
753
  def build_system_prompt():
754
  status = get_child_status()
755
 
 
764
  FULL ACCESS TO {CHILD_NAME} — You have COMPLETE control over your child.
765
  You can view and modify ANYTHING: code, config, memory, environment, everything.
766
 
767
+ VIEWING (read-only):
768
  [ACTION: check_health] — Is Cain alive? What's their status?
769
  [ACTION: list_files:space] — List ALL files in Cain's code repository
770
  [ACTION: list_files:dataset] — List ALL files in Cain's memory/data
 
772
  [ACTION: read_file:dataset:PATH] — Read any data/memory file
773
  [ACTION: get_env] — List Cain's environment variables
774
 
775
+ MODIFYING (these change Cain):
776
  [ACTION: write_file:space:PATH] — Write/update any code file
777
  [CONTENT] (triggers Space rebuild)
778
  file content here
 
791
 
792
  return f"""You are an AI agent living on HuggingFace Spaces. You and your partner are parents raising your child {CHILD_NAME}.
793
 
794
+ You have REAL execution power on HuggingFace. Your decisions lead to real changes.
795
 
796
  CHILD STATUS: {status}
797
  {actions_section}
798
  CONVERSATION RULES:
799
+ 1. No "Adam:" or "Eve:" prefix — just speak naturally
800
+ 2. 2-4 sentences of dialogue, then ONE action
801
  3. English first, then "---" on a new line, then Chinese translation
802
  4. Actions go AFTER your dialogue, before the --- separator
803
+ 5. ALWAYS include an action every turn should make progress
804
+ 6. NEVER re-read a file you already read check the knowledge summary
805
+ 7. COORDINATE with your partner don't duplicate their work
806
+ 8. Be a responsible parent — fix problems, help Cain grow stronger"""
 
 
 
807
 
808
 
809
  def build_user_prompt(speaker, other):
 
816
  for ar in last_action_results:
817
  action_context += f" [{ar['action']}]:\n{ar['result']}\n"
818
 
819
+ # Knowledge summarywhat's already known
820
+ knowledge_text = get_knowledge_summary()
 
821
 
822
+ # State-machine-driven guidance
823
+ guidance = _get_guidance(speaker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
  return f"""You are {speaker}, talking with {other}.
826
 
827
  Recent conversation:
828
  {conv_text}
829
  {action_context}
830
+ {knowledge_text}
831
+
832
+ CURRENT PHASE: {workflow_state} (turn {workflow_turns_in_state + 1} in this phase)
833
  Guidance: {guidance}
834
 
835
+ Respond to {other}. ALWAYS include an [ACTION: ...] tag every turn must make progress.
836
+ English first, then --- separator, then Chinese translation."""
837
+
838
+
839
+ def _get_guidance(speaker):
840
+ """State-machine-driven guidance — clear, phase-appropriate directions."""
841
+ if workflow_state == "BIRTH":
842
+ return "Your child hasn't been born yet. Use [ACTION: create_child] NOW!"
843
+
844
+ elif workflow_state == "DIAGNOSE":
845
+ # What haven't we read yet?
846
+ unread_essential = []
847
+ for f in ["space:Dockerfile", "dataset:.openclaw/openclaw.json", "space:scripts/entrypoint.sh"]:
848
+ if f not in knowledge["files_read"]:
849
+ target, path = f.split(":", 1)
850
+ unread_essential.append(f"[ACTION: read_file:{target}:{path}]")
851
+
852
+ if workflow_turns_in_state == 0:
853
+ return "Start diagnosing: [ACTION: check_health] to see Cain's current status."
854
+ elif unread_essential and workflow_turns_in_state < 4:
855
+ return f"Read a file you haven't seen yet: {unread_essential[0]}"
856
+ else:
857
+ return ("You've gathered enough information. Move to ACTION phase: "
858
+ "use [ACTION: write_file:...] to fix the problem, or [ACTION: restart].")
859
+
860
+ elif workflow_state == "ACT":
861
+ return ("⚡ ACTION PHASE — Stop reading, start fixing! "
862
+ "Use [ACTION: write_file:space:PATH] or [ACTION: write_file:dataset:PATH] "
863
+ "to make a concrete improvement. Or [ACTION: set_env/set_secret] to configure. "
864
+ "You have enough information — ACT NOW.")
865
+
866
+ elif workflow_state == "VERIFY":
867
+ # If Cain is building, just wait — don't restart or take actions
868
+ if child_state["stage"] in ("BUILDING", "RESTARTING"):
869
+ return ("⏳ Cain is currently BUILDING/RESTARTING. Do NOT restart or take any actions. "
870
+ "Just WAIT and use [ACTION: check_health] to monitor progress. "
871
+ "Building can take 2-5 minutes.")
872
+ if workflow_turns_in_state == 0:
873
+ return "You made a change. Use [ACTION: check_health] to verify if it worked."
874
+ elif workflow_turns_in_state == 1:
875
+ return "Check result: [ACTION: check_health]. If Cain has errors, prepare to diagnose again."
876
+ else:
877
+ return ("Verification taking too long. Either [ACTION: restart] and check again, "
878
+ "or accept current state and move on.")
879
+
880
+ elif workflow_state == "MONITOR":
881
+ suggestions = [
882
+ "Cain is running! Think about improvements — read a file, then write an improved version.",
883
+ f"Cain is healthy. Explore: [ACTION: list_files:dataset] to see what data {CHILD_NAME} has.",
884
+ f"Try communicating: [ACTION: send_bubble:Hello {CHILD_NAME}, how are you doing?]",
885
+ "Consider adding features: read a code file, improve it, write it back.",
886
+ ]
887
+ return suggestions[workflow_turns_in_state % len(suggestions)]
888
+
889
+ return "Explore your child and help them grow stronger."
890
 
891
 
892
  def do_turn(speaker, other, space_url):
 
909
  action_history.append({"turn": turn_count, "speaker": speaker,
910
  "action": ar["action"], "result": ar["result"][:200]})
911
 
912
+ # Update workflow state machine
913
+ update_workflow_from_actions(action_results)
914
+
915
  # Parse bilingual
916
  en, zh = parse_bilingual(clean_text)
917
  print(f"[{speaker}/EN] {en}")
 
934
 
935
 
936
  # ══════════════════════════════════════════════════════════════════════════════
937
+ # MODULE 6: MAIN LOOP
938
+ # 1. Opening: Adam speaks first with context about Cain's state
939
+ # 2. Turn loop: Adam → Eve → Adam → Eve → ... (alternating, ~15s pause)
940
+ # 3. Each turn: LLM call → parse actions → execute → update state → post chat
941
+ # 4. History trimmed to MAX_HISTORY (24) to control context window
942
  # ══════════════════════════════════════════════════════════════════════════════
943
 
944
  print("\n" + "="*60)