Spaces:
Paused
Paused
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 +19 -12
- scripts/conversation-loop.py +357 -67
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 |
-
|
| 413 |
-
|
|
|
|
| 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:
|
| 424 |
-
max-width:
|
| 425 |
justify-content: flex-start;
|
| 426 |
margin-top: 20px;
|
| 427 |
flex-wrap: wrap;
|
| 428 |
}
|
| 429 |
#chatlog-panel {
|
| 430 |
-
width:
|
| 431 |
flex-shrink: 0;
|
| 432 |
-
height:
|
|
|
|
|
|
|
| 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:
|
| 468 |
-
|
| 469 |
-
|
| 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
|
| 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:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
except:
|
| 188 |
pass
|
| 189 |
return (f"{CHILD_NAME} has a {stage}! "
|
| 190 |
f"Error: {error_detail or 'unknown'}. "
|
| 191 |
-
f"
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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.
|
| 527 |
|
| 528 |
CHILD STATUS: {status}
|
| 529 |
{actions_section}
|
| 530 |
CONVERSATION RULES:
|
| 531 |
-
1.
|
| 532 |
-
2. 2-4 sentences of dialogue, then
|
| 533 |
3. English first, then "---" on a new line, then Chinese translation
|
| 534 |
4. Actions go AFTER your dialogue, before the --- separator
|
| 535 |
-
5.
|
| 536 |
-
6.
|
| 537 |
-
7.
|
| 538 |
-
8. Be a responsible parent —
|
| 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 |
-
#
|
| 555 |
-
|
| 556 |
-
recent_actions = [ar["action"].split(":")[0] for ar in last_action_results] if last_action_results else []
|
| 557 |
|
| 558 |
-
#
|
| 559 |
-
|
| 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}.
|
| 605 |
-
English first, then --- separator, then Chinese translation.
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 summary — what'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)
|