Spaces:
Running
Running
feat: parallel discussion + async Claude Code execution
Browse filesArchitecture 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>
- 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 |
-
# β
|
| 26 |
-
# β
|
| 27 |
-
# β
|
| 28 |
-
# β
|
| 29 |
-
# β
|
| 30 |
-
# β
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 674 |
-
|
| 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
|
| 742 |
-
You do NOT code yourself. You discuss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
WORKFLOW EACH TURN:
|
| 745 |
-
1. Discuss with your partner (2-3 sentences) β
|
| 746 |
-
2.
|
|
|
|
| 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
|
| 773 |
-
2.
|
| 774 |
-
3.
|
| 775 |
-
4.
|
| 776 |
-
5.
|
| 777 |
-
6.
|
| 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 |
-
|
| 793 |
-
|
| 794 |
-
parts.append(
|
|
|
|
|
|
|
| 795 |
|
| 796 |
-
#
|
| 797 |
-
|
| 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 |
-
|
| 806 |
-
|
|
|
|
|
|
|
|
|
|
| 807 |
elif child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR", "CONFIG_ERROR"):
|
| 808 |
-
parts.append(f"\nπ¨ {CHILD_NAME} has {child_state['stage']}!
|
| 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
|
| 812 |
else:
|
| 813 |
-
parts.append(f"\nAnalyze the situation and
|
| 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(
|
| 875 |
|
| 876 |
|
| 877 |
def do_turn(speaker, other, space_url):
|
| 878 |
-
"""Execute one conversation turn."""
|
| 879 |
-
global last_action_results, turn_count,
|
| 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}:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 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(
|
| 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:]
|
|
|
|
|
|