tao-shen Claude Opus 4.6 commited on
Commit
426eeb0
Β·
1 Parent(s): 121b371

feat: parallel discussion + async Claude Code execution

Browse files

Architecture change: Adam/Eve no longer block waiting for Claude Code.

- CC runs in background thread via cc_submit_task()
- Agents discuss every 15s, seeing CC's live output in context
- cc_get_live_status() shows: WORKING (with recent output), FINISHED, or IDLE
- Agents assign [TASK] when CC is idle, discuss progress when CC is busy
- cc_live_lines deque shares streaming output between CC and discussion
- Removed nudge logic (not needed β€” discussion is natural when CC is busy)
- Main loop simplified: no blocking, no skipping turns

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

Files changed (1) hide show
  1. scripts/conversation-loop.py +138 -96
scripts/conversation-loop.py CHANGED
@@ -22,15 +22,17 @@ then delegate ALL coding work to Claude Code 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)
@@ -311,6 +313,7 @@ def action_send_bubble(text):
311
 
312
  CLAUDE_WORK_DIR = "/tmp/claude-workspace"
313
  CLAUDE_TIMEOUT = 300 # 5 minutes
 
314
 
315
  def action_claude_code(task):
316
  """Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
@@ -377,6 +380,7 @@ def action_claude_code(task):
377
  line = line.rstrip('\n')
378
  print(f" [CC] {line}")
379
  output_lines.append(line)
 
380
  if time.time() > deadline:
381
  proc.kill()
382
  output_lines.append("(killed: timeout)")
@@ -420,6 +424,64 @@ def action_claude_code(task):
420
  return f"=== Claude Code Output ===\n{output}\n\n=== Changes ===\n{push_result}"
421
 
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  # ══════════════════════════════════════════════════════════════════════════════
424
  # MODULE 3: CONTEXT GATHERING (automated, replaces LLM choosing read actions)
425
  # ══════════════════════════════════════════════════════════════════════════════
@@ -634,7 +696,7 @@ history = []
634
  MAX_HISTORY = 24
635
  last_action_results = []
636
  turn_count = 0
637
- last_claude_code_result = ""
638
 
639
  # Simple workflow state: BIRTH / WAITING / ACTIVE
640
  workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
@@ -670,13 +732,8 @@ def parse_and_execute_turn(raw_text, ctx):
670
  last_rebuild_trigger_at = 0
671
 
672
  if not results: # not blocked
673
- enriched = enrich_task_with_context(task_desc, ctx)
674
- print(f"[TASK] Sending to Claude Code ({len(enriched)} chars)...")
675
- result = action_claude_code(enriched)
676
- results.append({"action": "claude_code", "result": result})
677
- last_claude_code_result = result
678
- # Clear context cache since files may have changed
679
- _context_cache.clear()
680
 
681
  # 3. Handle [ACTION: restart] (escape hatch)
682
  if re.search(r'\[ACTION:\s*restart\]', raw_text):
@@ -738,12 +795,20 @@ You think about growth: is the code clean? Are there edge cases? What can be imp
738
  return f"""{role_desc.get(speaker, role_desc["Adam"])}
739
 
740
  You and your partner are parents of {CHILD_NAME}, working together to raise it.
741
- Claude Code is your engineer β€” it clones {CHILD_NAME}'s code, analyzes, fixes, and pushes changes.
742
- You do NOT code yourself. You discuss the situation with your partner, then assign Claude Code a task.
 
 
 
 
 
 
 
743
 
744
  WORKFLOW EACH TURN:
745
- 1. Discuss with your partner (2-3 sentences) β€” analyze the situation, respond to their observations
746
- 2. Then write a [TASK]...[/TASK] block assigning work to Claude Code β€” MANDATORY every turn
 
747
 
748
  IMPORTANT KNOWLEDGE β€” HuggingFace Spaces CONFIG_ERROR:
749
  - "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
@@ -769,12 +834,12 @@ HF SPACES TECHNICAL NOTES:
769
  - If sdk: gradio in README.md, Dockerfile is IGNORED. Use sdk: docker.
770
 
771
  OUTPUT FORMAT:
772
- 1. Discussion with partner (2-3 sentences analyzing the situation)
773
- 2. A [TASK]...[/TASK] block β€” MANDATORY every turn
774
- 3. Optional [ACTION: ...] if needed
775
- 4. English first, then --- separator, then Chinese translation
776
- 5. Be SPECIFIC in tasks β€” error messages, file names, expected behavior
777
- 6. If {CHILD_NAME} is BUILDING/RESTARTING, assign a review/planning/analysis task"""
778
 
779
 
780
  def build_user_prompt(speaker, other, ctx):
@@ -787,30 +852,33 @@ def build_user_prompt(speaker, other, ctx):
787
  for h in history[-8:]:
788
  parts.append(f"{h['speaker']}: {h['text'][:300]}")
789
 
790
- # Last action results
791
  if last_action_results:
792
- parts.append("\n=== LAST ACTION RESULTS ===")
793
- for ar in last_action_results:
794
- parts.append(f"[{ar['action']}]: {ar['result'][:500]}")
 
 
795
 
796
- # Last Claude Code result (if any)
797
- if last_claude_code_result:
798
- parts.append(f"\n=== LAST CLAUDE CODE RESULT ===\n{last_claude_code_result[:1500]}")
799
 
800
  # Auto-gathered context
801
  parts.append(f"\n=== {CHILD_NAME}'S CURRENT STATE (auto-gathered) ===")
802
  parts.append(format_context(ctx))
803
 
804
- # Guidance based on state
805
- if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
806
- parts.append(f"\n⏳ {CHILD_NAME} is {child_state['stage']}. Just discuss what you'll check next. Do NOT write a [TASK].")
 
 
 
807
  elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
808
- parts.append(f"\n🚨 {CHILD_NAME} has {child_state['stage']}! Analyze the context above and write a [TASK] for Claude Code to fix it.")
809
- parts.append("Be SPECIFIC: include the error message, relevant files, and what the fix should do.")
810
  elif child_state["alive"]:
811
- parts.append(f"\nβœ… {CHILD_NAME} is alive! Write a [TASK] for Claude Code to improve {CHILD_NAME} (add features, harden survival, etc).")
812
  else:
813
- parts.append(f"\nAnalyze the situation and decide what to do.")
814
 
815
  parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
816
  parts.append("English first, then --- separator, then Chinese translation.")
@@ -845,6 +913,7 @@ if child_state["created"]:
845
  else:
846
  opening = f"You and Eve need to create your first child. Use [ACTION: create_child] to bring them to life."
847
 
 
848
  reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
849
  if reply:
850
  clean, actions = parse_and_execute_turn(reply, ctx)
@@ -856,10 +925,6 @@ if reply:
856
  print(f"[Adam/ZH] {zh}")
857
  for ar in actions:
858
  print(f"[Adam/DID] {ar['action']}")
859
- if ar['action'] == 'claude_code':
860
- result_preview = ar['result'][:800].replace('\n', '\n ')
861
- print(f" [CC-RESULT] {result_preview}")
862
- import datetime
863
  ts = datetime.datetime.utcnow().strftime("%H:%M")
864
  entry = {"speaker": "Adam", "time": ts, "text": en, "text_zh": zh}
865
  if actions:
@@ -871,17 +936,22 @@ if reply:
871
  post_chatlog(history)
872
  persist_turn("Adam", 0, en, zh, actions, workflow_state, child_state["stage"])
873
 
874
- time.sleep(20)
875
 
876
 
877
  def do_turn(speaker, other, space_url):
878
- """Execute one conversation turn."""
879
- global last_action_results, turn_count, last_claude_code_result
880
  turn_count += 1
 
881
 
882
- # Auto-gather context
883
  ctx = gather_context()
884
 
 
 
 
 
885
  system = build_system_prompt(speaker)
886
  user = build_user_prompt(speaker, other, ctx)
887
  t0 = time.time()
@@ -893,20 +963,6 @@ def do_turn(speaker, other, space_url):
893
 
894
  clean_text, action_results = parse_and_execute_turn(raw_reply, ctx)
895
  elapsed = time.time() - t0
896
-
897
- # If no [TASK] was produced, retry once with a nudge
898
- has_task = any(ar['action'] == 'claude_code' for ar in action_results)
899
- if not has_task and not any(ar['action'] in ('create_child',) for ar in action_results):
900
- print(f"[{speaker}] No [TASK] found β€” nudging for a task...")
901
- nudge = call_llm(system, user + "\n\nIMPORTANT: You MUST include a [TASK]...[/TASK] block. "
902
- "Assign Claude Code something useful β€” review code, check configs, improve docs, anything. "
903
- "Do NOT just discuss. Output a [TASK] now.")
904
- if nudge and '[TASK]' in nudge:
905
- clean_text2, action_results2 = parse_and_execute_turn(nudge, ctx)
906
- clean_text = clean_text2
907
- action_results = action_results2
908
- has_task = True
909
-
910
  last_action_results = action_results
911
 
912
  en, zh = parse_bilingual(clean_text)
@@ -917,16 +973,17 @@ def do_turn(speaker, other, space_url):
917
  if action_results:
918
  for ar in action_results:
919
  print(f"[{speaker}/DID] {ar['action']}")
920
- # Log Claude Code result summary so agents can see what happened
921
- if ar['action'] == 'claude_code':
922
- result_preview = ar['result'][:800].replace('\n', '\n ')
923
- print(f" [CC-RESULT] {result_preview}")
924
  print(f"[{speaker}] Turn #{turn_count}: {len(action_results)} action(s) in {elapsed:.1f}s")
925
  else:
926
- print(f"[{speaker}] Turn #{turn_count}: no task assigned ({elapsed:.1f}s)")
 
 
 
 
 
 
927
 
928
  # Add to history with timestamp
929
- import datetime
930
  ts = datetime.datetime.utcnow().strftime("%H:%M")
931
  entry = {"speaker": speaker, "time": ts}
932
  if action_results:
@@ -942,40 +999,25 @@ def do_turn(speaker, other, space_url):
942
  return True
943
 
944
 
945
- # Main loop: Adam β†’ Eve β†’ Adam β†’ Eve β†’ ...
946
  while True:
947
- # Smart wait: if Cain is BUILDING/RESTARTING, skip Claude Code, just discuss
948
- if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
949
- check_and_clear_cooldown()
950
- try:
951
- info = hf_api.space_info(CHILD_SPACE_ID)
952
- new_stage = info.runtime.stage if info.runtime else "unknown"
953
- if new_stage != child_state["stage"]:
954
- print(f"[WAIT] Stage changed: {child_state['stage']} β†’ {new_stage}")
955
- child_state["stage"] = new_stage
956
- child_state["alive"] = (new_stage == "RUNNING")
957
- _context_cache.clear()
958
- else:
959
- print(f"[WAIT] Still {new_stage}... waiting 20s")
960
- time.sleep(20)
961
- continue
962
- except Exception as e:
963
- print(f"[WAIT] Health check error: {e}")
964
- time.sleep(20)
965
- continue
966
 
967
  do_turn("Eve", "Adam", EVE_SPACE)
968
- time.sleep(20)
969
-
970
- # Skip Adam if Claude Code just pushed (Cain will be rebuilding)
971
- if child_state["stage"] in ("BUILDING", "RESTARTING"):
972
- print(f"[SKIP] Cain entered {child_state['stage']} β€” skipping Adam's turn")
973
- time.sleep(10)
974
- continue
975
 
976
  do_turn("Adam", "Eve", ADAM_SPACE)
 
977
 
978
  if len(history) > MAX_HISTORY:
979
  history = history[-MAX_HISTORY:]
980
-
981
- time.sleep(20)
 
22
  # β•‘ β”‚ Cain Space β”‚ β”‚ (z.ai backend) β”‚ β•‘
23
  # β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘
24
  # β•‘ β•‘
25
+ # β•‘ Parallel flow: β•‘
26
+ # β•‘ DISCUSSION THREAD (every 15s): β•‘
27
+ # β•‘ Adam β†’ Eve β†’ Adam β†’ Eve β†’ ... (continuous) β•‘
28
+ # β•‘ Each turn sees CC's live output + Cain's state β•‘
29
+ # β•‘ CC WORKER THREAD (background): β•‘
30
+ # β•‘ Receives [TASK] β†’ clone β†’ analyze β†’ fix β†’ push β•‘
31
+ # β•‘ Streams output to shared buffer for agents to discuss β•‘
32
  # β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
33
  """
34
+ import json, time, re, requests, sys, os, io, subprocess, threading, datetime
35
+ from collections import deque
36
 
37
  # Force unbuffered output
38
  sys.stdout.reconfigure(line_buffering=True)
 
313
 
314
  CLAUDE_WORK_DIR = "/tmp/claude-workspace"
315
  CLAUDE_TIMEOUT = 300 # 5 minutes
316
+ TURN_INTERVAL = 15 # seconds between turns β€” fast enough for lively discussion
317
 
318
  def action_claude_code(task):
319
  """Run Claude Code CLI to autonomously complete a coding task on Cain's Space."""
 
380
  line = line.rstrip('\n')
381
  print(f" [CC] {line}")
382
  output_lines.append(line)
383
+ cc_live_lines.append(line)
384
  if time.time() > deadline:
385
  proc.kill()
386
  output_lines.append("(killed: timeout)")
 
424
  return f"=== Claude Code Output ===\n{output}\n\n=== Changes ===\n{push_result}"
425
 
426
 
427
+ # ── Background Claude Code Worker ────────────────────────────────────────────
428
+
429
+ cc_live_lines = deque(maxlen=30) # rolling window of CC output lines
430
+ cc_status = {"running": False, "task": "", "result": "", "assigned_by": "", "started": 0.0}
431
+ cc_lock = threading.Lock()
432
+
433
+
434
+ def cc_submit_task(task, assigned_by, ctx):
435
+ """Submit a task to Claude Code in background. Non-blocking."""
436
+ with cc_lock:
437
+ if cc_status["running"]:
438
+ return "BUSY: Claude Code is already working on a task. Wait for it to finish."
439
+ cc_status["running"] = True
440
+ cc_status["task"] = task[:200]
441
+ cc_status["result"] = ""
442
+ cc_status["assigned_by"] = assigned_by
443
+ cc_status["started"] = time.time()
444
+ cc_live_lines.clear()
445
+
446
+ enriched = enrich_task_with_context(task, ctx)
447
+ print(f"[TASK] {assigned_by} assigned to Claude Code ({len(enriched)} chars)...")
448
+
449
+ def worker():
450
+ result = action_claude_code(enriched)
451
+ with cc_lock:
452
+ cc_status["running"] = False
453
+ cc_status["result"] = result
454
+ print(f"[CC-DONE] Task from {assigned_by} finished ({len(result)} chars)")
455
+
456
+ t = threading.Thread(target=worker, daemon=True)
457
+ t.start()
458
+ return "Task submitted to Claude Code (running in background)."
459
+
460
+
461
+ def cc_get_live_status():
462
+ """Get CC's current status and recent output for agents to discuss."""
463
+ with cc_lock:
464
+ if cc_status["running"]:
465
+ elapsed = int(time.time() - cc_status["started"])
466
+ lines = list(cc_live_lines)
467
+ recent = "\n".join(lines[-10:]) if lines else "(no output yet)"
468
+ return (f"πŸ”¨ Claude Code is WORKING (assigned by {cc_status['assigned_by']}, {elapsed}s ago)\n"
469
+ f"Task: {cc_status['task']}\n"
470
+ f"Recent output:\n{recent}")
471
+ elif cc_status["result"]:
472
+ return (f"βœ… Claude Code FINISHED (assigned by {cc_status['assigned_by']})\n"
473
+ f"Result:\n{cc_status['result'][:1500]}")
474
+ else:
475
+ return "πŸ’€ Claude Code is IDLE β€” no active task."
476
+
477
+
478
+ # Patch action_claude_code to also feed cc_live_lines
479
+ _orig_cc_print = print
480
+ def _cc_line_hook(line):
481
+ """Called for each [CC] output line to feed the live buffer."""
482
+ cc_live_lines.append(line)
483
+
484
+
485
  # ══════════════════════════════════════════════════════════════════════════════
486
  # MODULE 3: CONTEXT GATHERING (automated, replaces LLM choosing read actions)
487
  # ══════════════════════════════════════════════════════════════════════════════
 
696
  MAX_HISTORY = 24
697
  last_action_results = []
698
  turn_count = 0
699
+ _current_speaker = "Adam"
700
 
701
  # Simple workflow state: BIRTH / WAITING / ACTIVE
702
  workflow_state = "BIRTH" if not child_state["created"] else "ACTIVE"
 
732
  last_rebuild_trigger_at = 0
733
 
734
  if not results: # not blocked
735
+ submit_result = cc_submit_task(task_desc, _current_speaker, ctx)
736
+ results.append({"action": "claude_code", "result": submit_result})
 
 
 
 
 
737
 
738
  # 3. Handle [ACTION: restart] (escape hatch)
739
  if re.search(r'\[ACTION:\s*restart\]', raw_text):
 
795
  return f"""{role_desc.get(speaker, role_desc["Adam"])}
796
 
797
  You and your partner are parents of {CHILD_NAME}, working together to raise it.
798
+ Claude Code is your engineer β€” it runs in the BACKGROUND while you keep discussing.
799
+ You do NOT code yourself. You discuss, observe Claude Code's progress, and assign new tasks.
800
+
801
+ HOW IT WORKS:
802
+ - Claude Code runs tasks IN THE BACKGROUND. You see its live output in the context.
803
+ - While Claude Code works, you keep discussing with your partner.
804
+ - When Claude Code finishes, review its results and assign the next task.
805
+ - If Claude Code is IDLE, assign a new [TASK].
806
+ - If Claude Code is BUSY, discuss its progress and plan what to do next.
807
 
808
  WORKFLOW EACH TURN:
809
+ 1. Discuss with your partner (2-3 sentences) β€” react to context, CC output, partner's observations
810
+ 2. If Claude Code is IDLE: write a [TASK]...[/TASK] to assign new work
811
+ 3. If Claude Code is BUSY: discuss its progress, no [TASK] needed
812
 
813
  IMPORTANT KNOWLEDGE β€” HuggingFace Spaces CONFIG_ERROR:
814
  - "Collision on variables and secrets names" = env VARIABLE and SECRET with SAME NAME.
 
834
  - If sdk: gradio in README.md, Dockerfile is IGNORED. Use sdk: docker.
835
 
836
  OUTPUT FORMAT:
837
+ 1. Discussion with partner (2-3 sentences) β€” respond to partner, react to CC output
838
+ 2. If CC is IDLE: a [TASK]...[/TASK] block to assign new work
839
+ 3. If CC is BUSY: no [TASK] needed, just discuss its progress
840
+ 4. Optional [ACTION: ...] if needed
841
+ 5. English first, then --- separator, then Chinese translation
842
+ 6. Be SPECIFIC in tasks β€” error messages, file names, expected behavior"""
843
 
844
 
845
  def build_user_prompt(speaker, other, ctx):
 
852
  for h in history[-8:]:
853
  parts.append(f"{h['speaker']}: {h['text'][:300]}")
854
 
855
+ # Last action results (non-CC)
856
  if last_action_results:
857
+ non_cc = [ar for ar in last_action_results if ar['action'] != 'claude_code']
858
+ if non_cc:
859
+ parts.append("\n=== LAST ACTION RESULTS ===")
860
+ for ar in non_cc:
861
+ parts.append(f"[{ar['action']}]: {ar['result'][:500]}")
862
 
863
+ # Claude Code live status (async)
864
+ parts.append(f"\n=== CLAUDE CODE STATUS ===\n{cc_get_live_status()}")
 
865
 
866
  # Auto-gathered context
867
  parts.append(f"\n=== {CHILD_NAME}'S CURRENT STATE (auto-gathered) ===")
868
  parts.append(format_context(ctx))
869
 
870
+ # Guidance based on CC status + child state
871
+ cc_busy = cc_status["running"]
872
+ if cc_busy:
873
+ parts.append(f"\nπŸ”¨ Claude Code is WORKING. Discuss its progress with your partner. No [TASK] needed now.")
874
+ elif child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
875
+ parts.append(f"\n⏳ {CHILD_NAME} is {child_state['stage']}. Discuss what to check next. Assign a review [TASK] if CC is idle.")
876
  elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
877
+ parts.append(f"\n🚨 {CHILD_NAME} has {child_state['stage']}! Write a [TASK] for Claude Code to fix it.")
 
878
  elif child_state["alive"]:
879
+ parts.append(f"\nβœ… {CHILD_NAME} is alive and CC is idle. Write a [TASK] to improve {CHILD_NAME}.")
880
  else:
881
+ parts.append(f"\nAnalyze the situation and write a [TASK] if CC is idle.")
882
 
883
  parts.append(f"\nYou are {speaker}. Your partner is {other}. Respond now.")
884
  parts.append("English first, then --- separator, then Chinese translation.")
 
913
  else:
914
  opening = f"You and Eve need to create your first child. Use [ACTION: create_child] to bring them to life."
915
 
916
+ _current_speaker = "Adam"
917
  reply = call_llm(build_system_prompt("Adam"), f"{opening}\n\n{format_context(ctx)}\n\nEnglish first, then --- separator, then Chinese translation.")
918
  if reply:
919
  clean, actions = parse_and_execute_turn(reply, ctx)
 
925
  print(f"[Adam/ZH] {zh}")
926
  for ar in actions:
927
  print(f"[Adam/DID] {ar['action']}")
 
 
 
 
928
  ts = datetime.datetime.utcnow().strftime("%H:%M")
929
  entry = {"speaker": "Adam", "time": ts, "text": en, "text_zh": zh}
930
  if actions:
 
936
  post_chatlog(history)
937
  persist_turn("Adam", 0, en, zh, actions, workflow_state, child_state["stage"])
938
 
939
+ time.sleep(TURN_INTERVAL)
940
 
941
 
942
  def do_turn(speaker, other, space_url):
943
+ """Execute one conversation turn (non-blocking β€” CC runs in background)."""
944
+ global last_action_results, turn_count, _current_speaker
945
  turn_count += 1
946
+ _current_speaker = speaker
947
 
948
+ # Auto-gather context (lightweight)
949
  ctx = gather_context()
950
 
951
+ # Check if CC just finished β€” clear result after agents see it once
952
+ with cc_lock:
953
+ cc_just_finished = (not cc_status["running"] and cc_status["result"])
954
+
955
  system = build_system_prompt(speaker)
956
  user = build_user_prompt(speaker, other, ctx)
957
  t0 = time.time()
 
963
 
964
  clean_text, action_results = parse_and_execute_turn(raw_reply, ctx)
965
  elapsed = time.time() - t0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
  last_action_results = action_results
967
 
968
  en, zh = parse_bilingual(clean_text)
 
973
  if action_results:
974
  for ar in action_results:
975
  print(f"[{speaker}/DID] {ar['action']}")
 
 
 
 
976
  print(f"[{speaker}] Turn #{turn_count}: {len(action_results)} action(s) in {elapsed:.1f}s")
977
  else:
978
+ print(f"[{speaker}] Turn #{turn_count}: discussion ({elapsed:.1f}s)")
979
+
980
+ # Clear CC result after both agents have had a chance to see it
981
+ if cc_just_finished and speaker == "Eve":
982
+ with cc_lock:
983
+ cc_status["result"] = ""
984
+ _context_cache.clear()
985
 
986
  # Add to history with timestamp
 
987
  ts = datetime.datetime.utcnow().strftime("%H:%M")
988
  entry = {"speaker": speaker, "time": ts}
989
  if action_results:
 
999
  return True
1000
 
1001
 
1002
+ # Main loop: Adam β†’ Eve β†’ Adam β†’ Eve β†’ ... (non-blocking, CC runs in background)
1003
  while True:
1004
+ # Refresh Cain's stage periodically
1005
+ try:
1006
+ info = hf_api.space_info(CHILD_SPACE_ID)
1007
+ new_stage = info.runtime.stage if info.runtime else "unknown"
1008
+ if new_stage != child_state["stage"]:
1009
+ print(f"[STATUS] {child_state['stage']} β†’ {new_stage}")
1010
+ child_state["stage"] = new_stage
1011
+ child_state["alive"] = (new_stage == "RUNNING")
1012
+ _context_cache.clear()
1013
+ except Exception as e:
1014
+ print(f"[STATUS] Error: {e}")
 
 
 
 
 
 
 
 
1015
 
1016
  do_turn("Eve", "Adam", EVE_SPACE)
1017
+ time.sleep(TURN_INTERVAL)
 
 
 
 
 
 
1018
 
1019
  do_turn("Adam", "Eve", ADAM_SPACE)
1020
+ time.sleep(TURN_INTERVAL)
1021
 
1022
  if len(history) > MAX_HISTORY:
1023
  history = history[-MAX_HISTORY:]