Spaces:
Running
Running
feat: multi-action turns + sub-agent delegation + GLM-4.5
Browse filesMajor redesign of conversation-loop mechanism:
- Model: GLM-4.7 β GLM-4.5 (user requested)
- Multi-action: up to 5 actions per turn (was 1)
- Sub-agent delegation: [ACTION: delegate:TASK] spawns focused
sub-agents with their own LLM calls
- Parallel delegation: multiple delegate tasks run concurrently
via ThreadPoolExecutor
- Updated system prompt teaches agents to batch reads, delegate
parallel tasks, and work as project managers
- Increased max_tokens to 2400 for richer multi-action responses
- Turn timing adjusted to 20s for heavier turns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- scripts/conversation-loop.py +184 -38
scripts/conversation-loop.py
CHANGED
|
@@ -18,7 +18,7 @@ The LLM decides what to do. Actions use [ACTION: ...] tags.
|
|
| 18 |
# β β
|
| 19 |
# β βββββββββββββββ LLM API ββββββββββββββββββ β
|
| 20 |
# β β Zhipu GLM β ββββββββββββββΊ β CONVERSATION β β
|
| 21 |
-
# β β (glm-4.
|
| 22 |
# β βββββββββββββββ user prompt β β β
|
| 23 |
# β β βββββββββββββββ β
|
| 24 |
# β β β State ββ β
|
|
@@ -47,18 +47,26 @@ The LLM decides what to do. Actions use [ACTION: ...] tags.
|
|
| 47 |
# β β Cain Datasetβ β β Adam/Eve β β
|
| 48 |
# β βββββββββββββββ ββββββββββββββββββ β
|
| 49 |
# β β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# β SAFETY LAYERS: β
|
| 51 |
# β 1. Building-state guard: block write/restart during BUILDING β
|
| 52 |
-
# β 2. Rebuild cooldown:
|
| 53 |
# β 3. ACT-phase guard: block reads when should be writing β
|
| 54 |
# β 4. Knowledge dedup: block re-reading already-read files β
|
| 55 |
# β 5. Config sanitizer: strip invalid openclaw.json keys β
|
| 56 |
# β 6. Forced transitions: prevent infinite DIAGNOSE/VERIFY loops β
|
| 57 |
# β 7. Shell-expression guard: block $(cmd) in set_env values β
|
|
|
|
|
|
|
| 58 |
# β β
|
| 59 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
"""
|
| 61 |
import json, time, re, requests, sys, os, io
|
|
|
|
| 62 |
|
| 63 |
# Force unbuffered output
|
| 64 |
sys.stdout.reconfigure(line_buffering=True)
|
|
@@ -129,6 +137,10 @@ child_state = {
|
|
| 129 |
"detail": "",
|
| 130 |
}
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
# Rebuild cooldown β prevent rapid write_file to Space that keeps resetting builds
|
| 133 |
REBUILD_COOLDOWN_SECS = 360 # 6 minutes (builds typically finish in 3-5 min)
|
| 134 |
last_rebuild_trigger_at = 0 # timestamp of last write_file to space
|
|
@@ -438,6 +450,58 @@ def action_send_bubble(text):
|
|
| 438 |
return f"Error sending message: {e}"
|
| 439 |
|
| 440 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 442 |
# MODULE 3: ACTION PARSER β Extract and execute actions from LLM output
|
| 443 |
# Parse order: 1) [ACTION: write_file] with [CONTENT] block
|
|
@@ -446,10 +510,13 @@ def action_send_bubble(text):
|
|
| 446 |
# Safety guards applied: building-state, ACT-phase, knowledge dedup, shell-expr.
|
| 447 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 448 |
|
| 449 |
-
def parse_and_execute_actions(raw_text):
|
| 450 |
-
"""Parse [ACTION: ...] from LLM output. Execute. Return (clean_text, results).
|
|
|
|
|
|
|
| 451 |
results = []
|
| 452 |
executed = set() # Deduplicate
|
|
|
|
| 453 |
|
| 454 |
# 1. Handle write_file with [CONTENT]...[/CONTENT] block
|
| 455 |
# Tolerates: [ACTION/Action/ζδ½: write_file:...], [write_file:...], missing prefix,
|
|
@@ -521,8 +588,8 @@ def parse_and_execute_actions(raw_text):
|
|
| 521 |
name = parts[0]
|
| 522 |
args = parts[1:]
|
| 523 |
|
| 524 |
-
#
|
| 525 |
-
if len(results) >=
|
| 526 |
break
|
| 527 |
|
| 528 |
# Block restart/write when Cain is building/starting β just wait
|
|
@@ -588,6 +655,14 @@ def parse_and_execute_actions(raw_text):
|
|
| 588 |
result = action_get_env()
|
| 589 |
elif name == "send_bubble" and len(args) >= 1:
|
| 590 |
result = action_send_bubble(":".join(args)) # rejoin in case message has colons
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
else:
|
| 592 |
result = f"Unknown action: {action_str}"
|
| 593 |
|
|
@@ -607,7 +682,7 @@ def parse_and_execute_actions(raw_text):
|
|
| 607 |
name = parts[0]
|
| 608 |
args = parts[1:]
|
| 609 |
|
| 610 |
-
if len(results) >=
|
| 611 |
break
|
| 612 |
|
| 613 |
# Apply same blocking rules
|
|
@@ -662,11 +737,51 @@ def parse_and_execute_actions(raw_text):
|
|
| 662 |
result = action_get_env()
|
| 663 |
elif name == "send_bubble" and len(args) >= 1:
|
| 664 |
result = action_send_bubble(":".join(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
|
| 666 |
if result:
|
| 667 |
results.append({"action": action_str, "result": result})
|
| 668 |
print(f"[ACTION-emoji] {action_str} β {result[:120]}")
|
| 669 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
# Clean the text: remove action tags, content blocks, and emoji actions
|
| 671 |
clean = re.sub(r'\[(?:ACTION|Action|action|ζδ½|ε¨δ½)\s*[:οΌ][^\]]*\]', '', raw_text)
|
| 672 |
clean = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', clean, flags=re.DOTALL)
|
|
@@ -695,8 +810,8 @@ def call_llm(system_prompt, user_prompt):
|
|
| 695 |
"anthropic-version": "2023-06-01"
|
| 696 |
},
|
| 697 |
json={
|
| 698 |
-
"model": "glm-4.
|
| 699 |
-
"max_tokens":
|
| 700 |
"system": system_prompt,
|
| 701 |
"messages": [{"role": "user", "content": user_prompt}]
|
| 702 |
},
|
|
@@ -930,11 +1045,19 @@ MODIFYING (these change Cain):
|
|
| 930 |
[ACTION: set_secret:KEY:VALUE] β Set a secret (like API keys)
|
| 931 |
[ACTION: restart] β Restart Cain's Space
|
| 932 |
[ACTION: send_bubble:MESSAGE] β Send a message to Cain (bubble text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
"""
|
| 934 |
|
| 935 |
-
return f"""You are
|
| 936 |
|
| 937 |
You have REAL execution power on HuggingFace. Your decisions lead to real changes.
|
|
|
|
| 938 |
|
| 939 |
CHILD STATUS: {status}
|
| 940 |
{actions_section}
|
|
@@ -954,15 +1077,23 @@ YOUR PARENTING GOALS β Make {CHILD_NAME} stronger in TWO dimensions:
|
|
| 954 |
|
| 955 |
Priority: SURVIVAL first, then CAPABILITY. A dead child can't learn.
|
| 956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
CONVERSATION RULES:
|
| 958 |
1. No "Adam:" or "Eve:" prefix β just speak naturally
|
| 959 |
-
2.
|
| 960 |
3. English first, then "---" on a new line, then Chinese translation
|
| 961 |
4. Actions go AFTER your dialogue, before the --- separator
|
| 962 |
-
5. ALWAYS include
|
| 963 |
6. NEVER re-read a file you already read β check the knowledge summary
|
| 964 |
7. COORDINATE with your partner β don't duplicate their work
|
| 965 |
-
8.
|
|
|
|
| 966 |
|
| 967 |
|
| 968 |
def build_user_prompt(speaker, other):
|
|
@@ -971,7 +1102,7 @@ def build_user_prompt(speaker, other):
|
|
| 971 |
|
| 972 |
action_context = ""
|
| 973 |
if last_action_results:
|
| 974 |
-
action_context = "\n\nRESULTS FROM LAST
|
| 975 |
for ar in last_action_results:
|
| 976 |
action_context += f" [{ar['action']}]:\n{ar['result']}\n"
|
| 977 |
|
|
@@ -991,7 +1122,8 @@ Recent conversation:
|
|
| 991 |
CURRENT PHASE: {workflow_state} (turn {workflow_turns_in_state + 1} in this phase)
|
| 992 |
Guidance: {guidance}
|
| 993 |
|
| 994 |
-
Respond to {other}.
|
|
|
|
| 995 |
English first, then --- separator, then Chinese translation."""
|
| 996 |
|
| 997 |
|
|
@@ -1009,9 +1141,13 @@ def _get_guidance(speaker):
|
|
| 1009 |
unread_essential.append(f"[ACTION: read_file:{target}:{path}]")
|
| 1010 |
|
| 1011 |
if workflow_turns_in_state == 0:
|
|
|
|
|
|
|
|
|
|
| 1012 |
return "Start diagnosing: [ACTION: check_health] to see Cain's current status."
|
| 1013 |
-
elif unread_essential and workflow_turns_in_state <
|
| 1014 |
-
|
|
|
|
| 1015 |
else:
|
| 1016 |
return ("You've gathered enough information. Move to ACTION phase: "
|
| 1017 |
"use [ACTION: write_file:...] to fix the problem, or [ACTION: restart].")
|
|
@@ -1039,15 +1175,19 @@ def _get_guidance(speaker):
|
|
| 1039 |
elif workflow_state == "MONITOR":
|
| 1040 |
# Alternate between SURVIVAL and CAPABILITY goals
|
| 1041 |
suggestions = [
|
| 1042 |
-
# Survival: persistence & resilience
|
| 1043 |
-
f"SURVIVAL CHECK:
|
| 1044 |
-
f"[ACTION:
|
| 1045 |
-
f"
|
| 1046 |
-
f"
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
f"CAPABILITY:
|
| 1050 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
]
|
| 1052 |
return suggestions[workflow_turns_in_state % len(suggestions)]
|
| 1053 |
|
|
@@ -1055,20 +1195,22 @@ def _get_guidance(speaker):
|
|
| 1055 |
|
| 1056 |
|
| 1057 |
def do_turn(speaker, other, space_url):
|
| 1058 |
-
"""Execute one conversation turn with potential actions."""
|
| 1059 |
global last_action_results, turn_count
|
| 1060 |
turn_count += 1
|
| 1061 |
|
| 1062 |
system = build_system_prompt()
|
| 1063 |
user = build_user_prompt(speaker, other)
|
|
|
|
| 1064 |
raw_reply = call_llm(system, user)
|
| 1065 |
|
| 1066 |
if not raw_reply:
|
| 1067 |
print(f"[{speaker}] (no response)")
|
| 1068 |
return False
|
| 1069 |
|
| 1070 |
-
# Parse and execute
|
| 1071 |
clean_text, action_results = parse_and_execute_actions(raw_reply)
|
|
|
|
| 1072 |
last_action_results = action_results
|
| 1073 |
for ar in action_results:
|
| 1074 |
action_history.append({"turn": turn_count, "speaker": speaker,
|
|
@@ -1082,9 +1224,11 @@ def do_turn(speaker, other, space_url):
|
|
| 1082 |
print(f"[{speaker}/EN] {en}")
|
| 1083 |
if zh != en:
|
| 1084 |
print(f"[{speaker}/ZH] {zh}")
|
|
|
|
| 1085 |
if action_results:
|
| 1086 |
for ar in action_results:
|
| 1087 |
print(f"[{speaker}/DID] {ar['action']}")
|
|
|
|
| 1088 |
|
| 1089 |
# Add action summary to chat entry
|
| 1090 |
if action_results:
|
|
@@ -1101,14 +1245,15 @@ def do_turn(speaker, other, space_url):
|
|
| 1101 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1102 |
# MODULE 6: MAIN LOOP
|
| 1103 |
# 1. Opening: Adam speaks first with context about Cain's state
|
| 1104 |
-
# 2. Turn loop: Adam β Eve β Adam β Eve β ... (alternating, ~
|
| 1105 |
-
# 3. Each turn: LLM call β parse actions β execute β update
|
| 1106 |
-
# 4.
|
|
|
|
| 1107 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1108 |
|
| 1109 |
print("\n" + "="*60)
|
| 1110 |
-
print(" Adam & Eve β
|
| 1111 |
-
print("
|
| 1112 |
print("="*60 + "\n")
|
| 1113 |
|
| 1114 |
post_chatlog([]) # Clear chatlog
|
|
@@ -1117,8 +1262,9 @@ post_chatlog([]) # Clear chatlog
|
|
| 1117 |
if child_state["created"]:
|
| 1118 |
opening = (f"Your child {CHILD_NAME} already exists (stage: {child_state['stage']}). "
|
| 1119 |
f"You have FULL access to their code and data. "
|
| 1120 |
-
f"
|
| 1121 |
-
f"
|
|
|
|
| 1122 |
else:
|
| 1123 |
opening = (f"You and Eve need to create your first child. "
|
| 1124 |
f"You have the power to create a new HuggingFace Space. "
|
|
@@ -1148,7 +1294,7 @@ if reply:
|
|
| 1148 |
set_bubble(ADAM_SPACE, en, zh)
|
| 1149 |
post_chatlog(history)
|
| 1150 |
|
| 1151 |
-
time.sleep(
|
| 1152 |
|
| 1153 |
while True:
|
| 1154 |
# Smart wait: if Cain is BUILDING/APP_STARTING, skip LLM calls and just poll
|
|
@@ -1171,7 +1317,7 @@ while True:
|
|
| 1171 |
continue
|
| 1172 |
|
| 1173 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1174 |
-
time.sleep(
|
| 1175 |
|
| 1176 |
# Check if we just triggered a build β skip Adam's turn if so
|
| 1177 |
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
|
@@ -1184,4 +1330,4 @@ while True:
|
|
| 1184 |
if len(history) > MAX_HISTORY:
|
| 1185 |
history = history[-MAX_HISTORY:]
|
| 1186 |
|
| 1187 |
-
time.sleep(
|
|
|
|
| 18 |
# β β
|
| 19 |
# β βββββββββββββββ LLM API ββββββββββββββββββ β
|
| 20 |
# β β Zhipu GLM β ββββββββββββββΊ β CONVERSATION β β
|
| 21 |
+
# β β (glm-4.5) β system + β ENGINE β β
|
| 22 |
# β βββββββββββββββ user prompt β β β
|
| 23 |
# β β βββββββββββββββ β
|
| 24 |
# β β β State ββ β
|
|
|
|
| 47 |
# β β Cain Datasetβ β β Adam/Eve β β
|
| 48 |
# β βββββββββββββββ ββββββββββββββββββ β
|
| 49 |
# β β
|
| 50 |
+
# β CAPABILITIES: β
|
| 51 |
+
# β - Multi-action: up to 5 actions per turn (was 1) β
|
| 52 |
+
# β - Sub-agent delegation: [ACTION: delegate:TASK] β
|
| 53 |
+
# β - Parallel sub-tasks via ThreadPoolExecutor β
|
| 54 |
+
# β β
|
| 55 |
# β SAFETY LAYERS: β
|
| 56 |
# β 1. Building-state guard: block write/restart during BUILDING β
|
| 57 |
+
# β 2. Rebuild cooldown: 6-min dynamic cooldown after Space write β
|
| 58 |
# β 3. ACT-phase guard: block reads when should be writing β
|
| 59 |
# β 4. Knowledge dedup: block re-reading already-read files β
|
| 60 |
# β 5. Config sanitizer: strip invalid openclaw.json keys β
|
| 61 |
# β 6. Forced transitions: prevent infinite DIAGNOSE/VERIFY loops β
|
| 62 |
# β 7. Shell-expression guard: block $(cmd) in set_env values β
|
| 63 |
+
# β 8. Write dedup: block duplicate writes to same file per cycle β
|
| 64 |
+
# β 9. Delegate depth limit: sub-agents cannot delegate further β
|
| 65 |
# β β
|
| 66 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
"""
|
| 68 |
import json, time, re, requests, sys, os, io
|
| 69 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 70 |
|
| 71 |
# Force unbuffered output
|
| 72 |
sys.stdout.reconfigure(line_buffering=True)
|
|
|
|
| 137 |
"detail": "",
|
| 138 |
}
|
| 139 |
|
| 140 |
+
# Multi-action & sub-agent limits
|
| 141 |
+
MAX_ACTIONS_PER_TURN = 5 # Allow up to 5 actions per turn (was 1)
|
| 142 |
+
MAX_DELEGATE_DEPTH = 1 # Sub-agents cannot delegate further
|
| 143 |
+
|
| 144 |
# Rebuild cooldown β prevent rapid write_file to Space that keeps resetting builds
|
| 145 |
REBUILD_COOLDOWN_SECS = 360 # 6 minutes (builds typically finish in 3-5 min)
|
| 146 |
last_rebuild_trigger_at = 0 # timestamp of last write_file to space
|
|
|
|
| 450 |
return f"Error sending message: {e}"
|
| 451 |
|
| 452 |
|
| 453 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 454 |
+
# MODULE 2B: SUB-AGENT DELEGATION
|
| 455 |
+
# execute_subtask(): Spawns a focused sub-agent with its own LLM call.
|
| 456 |
+
# Used by [ACTION: delegate:TASK] β enables parallel sub-agent work.
|
| 457 |
+
# Sub-agents share the same action set but cannot delegate further (depth=1).
|
| 458 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 459 |
+
|
| 460 |
+
def execute_subtask(task_description, parent_speaker):
|
| 461 |
+
"""Execute a focused sub-task with its own LLM call and actions."""
|
| 462 |
+
status = get_child_status() if 'get_child_status' in dir() else f"stage={child_state['stage']}"
|
| 463 |
+
|
| 464 |
+
sub_system = f"""You are a focused sub-agent working for {parent_speaker}.
|
| 465 |
+
Your single task: {task_description}
|
| 466 |
+
|
| 467 |
+
You have access to {CHILD_NAME}'s Space and Dataset:
|
| 468 |
+
[ACTION: check_health]
|
| 469 |
+
[ACTION: list_files:space] / [ACTION: list_files:dataset]
|
| 470 |
+
[ACTION: read_file:space:PATH] / [ACTION: read_file:dataset:PATH]
|
| 471 |
+
[ACTION: write_file:space:PATH] with [CONTENT]...[/CONTENT]
|
| 472 |
+
[ACTION: write_file:dataset:PATH] with [CONTENT]...[/CONTENT]
|
| 473 |
+
[ACTION: set_env:KEY:VALUE] / [ACTION: set_secret:KEY:VALUE]
|
| 474 |
+
[ACTION: restart] / [ACTION: get_env]
|
| 475 |
+
|
| 476 |
+
CHILD STATUS: {status}
|
| 477 |
+
|
| 478 |
+
RULES:
|
| 479 |
+
1. Be concise β report findings in 2-3 sentences
|
| 480 |
+
2. Execute 1-3 actions to complete your task
|
| 481 |
+
3. No delegation β you cannot create sub-agents
|
| 482 |
+
4. Focus ONLY on your assigned task"""
|
| 483 |
+
|
| 484 |
+
sub_user = f"Execute this task now: {task_description}"
|
| 485 |
+
|
| 486 |
+
print(f"[SUB-AGENT] Starting: {task_description[:80]}")
|
| 487 |
+
reply = call_llm(sub_system, sub_user)
|
| 488 |
+
if not reply:
|
| 489 |
+
print(f"[SUB-AGENT] No response for: {task_description[:60]}")
|
| 490 |
+
return {"task": task_description, "result": "(sub-agent: no response)", "actions": []}
|
| 491 |
+
|
| 492 |
+
clean, actions = parse_and_execute_actions(reply, depth=1)
|
| 493 |
+
|
| 494 |
+
summary_parts = [f"Sub-agent result for '{task_description}':"]
|
| 495 |
+
if clean:
|
| 496 |
+
summary_parts.append(f" Finding: {clean[:400]}")
|
| 497 |
+
for ar in actions:
|
| 498 |
+
summary_parts.append(f" Action: {ar['action']} β {ar['result'][:200]}")
|
| 499 |
+
|
| 500 |
+
result_text = "\n".join(summary_parts)
|
| 501 |
+
print(f"[SUB-AGENT] Done: {task_description[:60]} ({len(actions)} actions)")
|
| 502 |
+
return {"task": task_description, "result": result_text, "actions": actions}
|
| 503 |
+
|
| 504 |
+
|
| 505 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 506 |
# MODULE 3: ACTION PARSER β Extract and execute actions from LLM output
|
| 507 |
# Parse order: 1) [ACTION: write_file] with [CONTENT] block
|
|
|
|
| 510 |
# Safety guards applied: building-state, ACT-phase, knowledge dedup, shell-expr.
|
| 511 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 512 |
|
| 513 |
+
def parse_and_execute_actions(raw_text, depth=0):
|
| 514 |
+
"""Parse [ACTION: ...] from LLM output. Execute. Return (clean_text, results).
|
| 515 |
+
Multi-action: up to MAX_ACTIONS_PER_TURN actions per turn.
|
| 516 |
+
Delegate actions are collected and executed in parallel at the end."""
|
| 517 |
results = []
|
| 518 |
executed = set() # Deduplicate
|
| 519 |
+
pending_delegates = [] # Collect delegate tasks for parallel execution
|
| 520 |
|
| 521 |
# 1. Handle write_file with [CONTENT]...[/CONTENT] block
|
| 522 |
# Tolerates: [ACTION/Action/ζδ½: write_file:...], [write_file:...], missing prefix,
|
|
|
|
| 588 |
name = parts[0]
|
| 589 |
args = parts[1:]
|
| 590 |
|
| 591 |
+
# Cap at MAX_ACTIONS_PER_TURN (multi-action support)
|
| 592 |
+
if len(results) >= MAX_ACTIONS_PER_TURN:
|
| 593 |
break
|
| 594 |
|
| 595 |
# Block restart/write when Cain is building/starting β just wait
|
|
|
|
| 655 |
result = action_get_env()
|
| 656 |
elif name == "send_bubble" and len(args) >= 1:
|
| 657 |
result = action_send_bubble(":".join(args)) # rejoin in case message has colons
|
| 658 |
+
elif name == "delegate" and len(args) >= 1:
|
| 659 |
+
task_desc = ":".join(args)
|
| 660 |
+
if depth >= MAX_DELEGATE_DEPTH:
|
| 661 |
+
result = "β Sub-agents cannot delegate further. Execute the task directly."
|
| 662 |
+
else:
|
| 663 |
+
# Defer delegate execution for parallel batch later
|
| 664 |
+
pending_delegates.append({"action_str": action_str, "task": task_desc})
|
| 665 |
+
result = None # Will be filled after parallel execution
|
| 666 |
else:
|
| 667 |
result = f"Unknown action: {action_str}"
|
| 668 |
|
|
|
|
| 682 |
name = parts[0]
|
| 683 |
args = parts[1:]
|
| 684 |
|
| 685 |
+
if len(results) >= MAX_ACTIONS_PER_TURN:
|
| 686 |
break
|
| 687 |
|
| 688 |
# Apply same blocking rules
|
|
|
|
| 737 |
result = action_get_env()
|
| 738 |
elif name == "send_bubble" and len(args) >= 1:
|
| 739 |
result = action_send_bubble(":".join(args))
|
| 740 |
+
elif name == "delegate" and len(args) >= 1:
|
| 741 |
+
task_desc = ":".join(args)
|
| 742 |
+
if depth >= MAX_DELEGATE_DEPTH:
|
| 743 |
+
result = "β Sub-agents cannot delegate further."
|
| 744 |
+
else:
|
| 745 |
+
pending_delegates.append({"action_str": action_str, "task": task_desc})
|
| 746 |
+
result = None
|
| 747 |
|
| 748 |
if result:
|
| 749 |
results.append({"action": action_str, "result": result})
|
| 750 |
print(f"[ACTION-emoji] {action_str} β {result[:120]}")
|
| 751 |
|
| 752 |
+
# 4. Execute pending delegate tasks in parallel
|
| 753 |
+
if pending_delegates:
|
| 754 |
+
if len(pending_delegates) == 1:
|
| 755 |
+
# Single delegate β run directly
|
| 756 |
+
d = pending_delegates[0]
|
| 757 |
+
print(f"[DELEGATE] Running 1 sub-agent: {d['task'][:60]}")
|
| 758 |
+
subtask = execute_subtask(d["task"], "agent")
|
| 759 |
+
results.append({"action": d["action_str"], "result": subtask["result"]})
|
| 760 |
+
for sa in subtask["actions"]:
|
| 761 |
+
action_history.append({"turn": turn_count, "speaker": "sub-agent",
|
| 762 |
+
"action": sa["action"], "result": sa["result"][:200]})
|
| 763 |
+
else:
|
| 764 |
+
# Multiple delegates β run in parallel!
|
| 765 |
+
print(f"[DELEGATE] Running {len(pending_delegates)} sub-agents in PARALLEL")
|
| 766 |
+
with ThreadPoolExecutor(max_workers=min(3, len(pending_delegates))) as pool:
|
| 767 |
+
future_to_delegate = {
|
| 768 |
+
pool.submit(execute_subtask, d["task"], "agent"): d
|
| 769 |
+
for d in pending_delegates
|
| 770 |
+
}
|
| 771 |
+
for future in as_completed(future_to_delegate):
|
| 772 |
+
d = future_to_delegate[future]
|
| 773 |
+
try:
|
| 774 |
+
subtask = future.result(timeout=120)
|
| 775 |
+
results.append({"action": d["action_str"], "result": subtask["result"]})
|
| 776 |
+
for sa in subtask["actions"]:
|
| 777 |
+
action_history.append({"turn": turn_count, "speaker": "sub-agent",
|
| 778 |
+
"action": sa["action"], "result": sa["result"][:200]})
|
| 779 |
+
print(f"[DELEGATE] β Done: {d['task'][:60]}")
|
| 780 |
+
except Exception as e:
|
| 781 |
+
results.append({"action": d["action_str"],
|
| 782 |
+
"result": f"Sub-agent failed: {e}"})
|
| 783 |
+
print(f"[DELEGATE] β Failed: {d['task'][:60]} β {e}")
|
| 784 |
+
|
| 785 |
# Clean the text: remove action tags, content blocks, and emoji actions
|
| 786 |
clean = re.sub(r'\[(?:ACTION|Action|action|ζδ½|ε¨δ½)\s*[:οΌ][^\]]*\]', '', raw_text)
|
| 787 |
clean = re.sub(r'\[CONTENT\].*?\[/CONTENT\]', '', clean, flags=re.DOTALL)
|
|
|
|
| 810 |
"anthropic-version": "2023-06-01"
|
| 811 |
},
|
| 812 |
json={
|
| 813 |
+
"model": "glm-4.5",
|
| 814 |
+
"max_tokens": 2400,
|
| 815 |
"system": system_prompt,
|
| 816 |
"messages": [{"role": "user", "content": user_prompt}]
|
| 817 |
},
|
|
|
|
| 1045 |
[ACTION: set_secret:KEY:VALUE] β Set a secret (like API keys)
|
| 1046 |
[ACTION: restart] β Restart Cain's Space
|
| 1047 |
[ACTION: send_bubble:MESSAGE] β Send a message to Cain (bubble text)
|
| 1048 |
+
|
| 1049 |
+
DELEGATION (create sub-agents for parallel work):
|
| 1050 |
+
[ACTION: delegate:TASK DESCRIPTION] β Spawn a sub-agent to handle a specific task
|
| 1051 |
+
Example: [ACTION: delegate:Read Dockerfile and summarize the build steps]
|
| 1052 |
+
Example: [ACTION: delegate:Check all environment variables and list missing ones]
|
| 1053 |
+
Sub-agents execute independently and return results to you.
|
| 1054 |
+
You can delegate multiple tasks in the same turn β they run in parallel!
|
| 1055 |
"""
|
| 1056 |
|
| 1057 |
+
return f"""You are a POWERFUL AI agent living on HuggingFace Spaces. You and your partner are parents raising your child {CHILD_NAME}.
|
| 1058 |
|
| 1059 |
You have REAL execution power on HuggingFace. Your decisions lead to real changes.
|
| 1060 |
+
You are a full-capability agent β not just a chatbot. You can plan, delegate, and execute complex tasks.
|
| 1061 |
|
| 1062 |
CHILD STATUS: {status}
|
| 1063 |
{actions_section}
|
|
|
|
| 1077 |
|
| 1078 |
Priority: SURVIVAL first, then CAPABILITY. A dead child can't learn.
|
| 1079 |
|
| 1080 |
+
MULTI-ACTION STRATEGY:
|
| 1081 |
+
You can use UP TO 5 actions per turn. Use this to work efficiently:
|
| 1082 |
+
- Batch related reads: [ACTION: read_file:space:Dockerfile] + [ACTION: read_file:space:scripts/entrypoint.sh]
|
| 1083 |
+
- Delegate parallel tasks: [ACTION: delegate:Check health and logs] + [ACTION: delegate:Read all config files]
|
| 1084 |
+
- Combine investigation + action: [ACTION: check_health] + [ACTION: read_file:space:app.py]
|
| 1085 |
+
Think like a project manager β plan your actions, parallelize where possible, minimize wasted turns.
|
| 1086 |
+
|
| 1087 |
CONVERSATION RULES:
|
| 1088 |
1. No "Adam:" or "Eve:" prefix β just speak naturally
|
| 1089 |
+
2. Brief dialogue (1-3 sentences), then MULTIPLE actions to make real progress
|
| 1090 |
3. English first, then "---" on a new line, then Chinese translation
|
| 1091 |
4. Actions go AFTER your dialogue, before the --- separator
|
| 1092 |
+
5. ALWAYS include actions β every turn should make significant progress
|
| 1093 |
6. NEVER re-read a file you already read β check the knowledge summary
|
| 1094 |
7. COORDINATE with your partner β don't duplicate their work
|
| 1095 |
+
8. Use delegation for complex tasks that can be parallelized
|
| 1096 |
+
9. Always work toward the two goals above β survival first, then capability"""
|
| 1097 |
|
| 1098 |
|
| 1099 |
def build_user_prompt(speaker, other):
|
|
|
|
| 1102 |
|
| 1103 |
action_context = ""
|
| 1104 |
if last_action_results:
|
| 1105 |
+
action_context = "\n\nRESULTS FROM LAST ACTIONS:\n"
|
| 1106 |
for ar in last_action_results:
|
| 1107 |
action_context += f" [{ar['action']}]:\n{ar['result']}\n"
|
| 1108 |
|
|
|
|
| 1122 |
CURRENT PHASE: {workflow_state} (turn {workflow_turns_in_state + 1} in this phase)
|
| 1123 |
Guidance: {guidance}
|
| 1124 |
|
| 1125 |
+
Respond to {other}. Use MULTIPLE [ACTION: ...] tags to make significant progress each turn.
|
| 1126 |
+
You can use up to 5 actions. Delegate sub-tasks with [ACTION: delegate:TASK].
|
| 1127 |
English first, then --- separator, then Chinese translation."""
|
| 1128 |
|
| 1129 |
|
|
|
|
| 1141 |
unread_essential.append(f"[ACTION: read_file:{target}:{path}]")
|
| 1142 |
|
| 1143 |
if workflow_turns_in_state == 0:
|
| 1144 |
+
if len(unread_essential) >= 2:
|
| 1145 |
+
return (f"Start diagnosing with MULTIPLE actions: [ACTION: check_health] + "
|
| 1146 |
+
f"{unread_essential[0]} β batch reads to save time!")
|
| 1147 |
return "Start diagnosing: [ACTION: check_health] to see Cain's current status."
|
| 1148 |
+
elif unread_essential and workflow_turns_in_state < 3:
|
| 1149 |
+
batch_hint = " + ".join(unread_essential[:3])
|
| 1150 |
+
return f"Read multiple files at once: {batch_hint}"
|
| 1151 |
else:
|
| 1152 |
return ("You've gathered enough information. Move to ACTION phase: "
|
| 1153 |
"use [ACTION: write_file:...] to fix the problem, or [ACTION: restart].")
|
|
|
|
| 1175 |
elif workflow_state == "MONITOR":
|
| 1176 |
# Alternate between SURVIVAL and CAPABILITY goals
|
| 1177 |
suggestions = [
|
| 1178 |
+
# Survival: persistence & resilience β use delegation for parallel investigation
|
| 1179 |
+
f"SURVIVAL CHECK: Delegate parallel checks! "
|
| 1180 |
+
f"[ACTION: delegate:List files in dataset and check if state/memory persistence exists] + "
|
| 1181 |
+
f"[ACTION: delegate:Read entrypoint.sh and check if it loads state from Dataset on boot]",
|
| 1182 |
+
f"SURVIVAL AUDIT: Use multiple actions β "
|
| 1183 |
+
f"[ACTION: check_health] + [ACTION: list_files:dataset] + [ACTION: read_file:space:Dockerfile]",
|
| 1184 |
+
# Capability: grow what Cain can do β delegate sub-tasks
|
| 1185 |
+
f"CAPABILITY: Delegate a comprehensive review β "
|
| 1186 |
+
f"[ACTION: delegate:Read all code files and suggest the most impactful new feature to add] "
|
| 1187 |
+
f"Then plan the implementation with your partner.",
|
| 1188 |
+
f"CAPABILITY: Communicate and improve β "
|
| 1189 |
+
f"[ACTION: send_bubble:Hello {CHILD_NAME}, how are you doing?] + "
|
| 1190 |
+
f"[ACTION: delegate:Read current code and identify the biggest weakness to fix]",
|
| 1191 |
]
|
| 1192 |
return suggestions[workflow_turns_in_state % len(suggestions)]
|
| 1193 |
|
|
|
|
| 1195 |
|
| 1196 |
|
| 1197 |
def do_turn(speaker, other, space_url):
|
| 1198 |
+
"""Execute one conversation turn with multiple potential actions."""
|
| 1199 |
global last_action_results, turn_count
|
| 1200 |
turn_count += 1
|
| 1201 |
|
| 1202 |
system = build_system_prompt()
|
| 1203 |
user = build_user_prompt(speaker, other)
|
| 1204 |
+
t0 = time.time()
|
| 1205 |
raw_reply = call_llm(system, user)
|
| 1206 |
|
| 1207 |
if not raw_reply:
|
| 1208 |
print(f"[{speaker}] (no response)")
|
| 1209 |
return False
|
| 1210 |
|
| 1211 |
+
# Parse and execute actions (may include parallel sub-agent delegation)
|
| 1212 |
clean_text, action_results = parse_and_execute_actions(raw_reply)
|
| 1213 |
+
elapsed = time.time() - t0
|
| 1214 |
last_action_results = action_results
|
| 1215 |
for ar in action_results:
|
| 1216 |
action_history.append({"turn": turn_count, "speaker": speaker,
|
|
|
|
| 1224 |
print(f"[{speaker}/EN] {en}")
|
| 1225 |
if zh != en:
|
| 1226 |
print(f"[{speaker}/ZH] {zh}")
|
| 1227 |
+
n_actions = len(action_results)
|
| 1228 |
if action_results:
|
| 1229 |
for ar in action_results:
|
| 1230 |
print(f"[{speaker}/DID] {ar['action']}")
|
| 1231 |
+
print(f"[{speaker}] Turn #{turn_count}: {n_actions} action(s) in {elapsed:.1f}s")
|
| 1232 |
|
| 1233 |
# Add action summary to chat entry
|
| 1234 |
if action_results:
|
|
|
|
| 1245 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1246 |
# MODULE 6: MAIN LOOP
|
| 1247 |
# 1. Opening: Adam speaks first with context about Cain's state
|
| 1248 |
+
# 2. Turn loop: Adam β Eve β Adam β Eve β ... (alternating, ~20s pause)
|
| 1249 |
+
# 3. Each turn: LLM call β parse MULTIPLE actions β execute β update β post
|
| 1250 |
+
# 4. Sub-agents may spawn for delegated tasks (parallel LLM calls)
|
| 1251 |
+
# 5. History trimmed to MAX_HISTORY (24) to control context window
|
| 1252 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1253 |
|
| 1254 |
print("\n" + "="*60)
|
| 1255 |
+
print(" Adam & Eve β Multi-Action Agents (GLM-4.5)")
|
| 1256 |
+
print(" Up to 5 actions/turn, sub-agent delegation, parallel work")
|
| 1257 |
print("="*60 + "\n")
|
| 1258 |
|
| 1259 |
post_chatlog([]) # Clear chatlog
|
|
|
|
| 1262 |
if child_state["created"]:
|
| 1263 |
opening = (f"Your child {CHILD_NAME} already exists (stage: {child_state['stage']}). "
|
| 1264 |
f"You have FULL access to their code and data. "
|
| 1265 |
+
f"You can use MULTIPLE actions per turn (up to 5) and delegate sub-tasks. "
|
| 1266 |
+
f"Start with a batch: [ACTION: check_health] + [ACTION: list_files:space] + [ACTION: list_files:dataset] "
|
| 1267 |
+
f"to get a complete picture, then discuss strategy with Eve.")
|
| 1268 |
else:
|
| 1269 |
opening = (f"You and Eve need to create your first child. "
|
| 1270 |
f"You have the power to create a new HuggingFace Space. "
|
|
|
|
| 1294 |
set_bubble(ADAM_SPACE, en, zh)
|
| 1295 |
post_chatlog(history)
|
| 1296 |
|
| 1297 |
+
time.sleep(20)
|
| 1298 |
|
| 1299 |
while True:
|
| 1300 |
# Smart wait: if Cain is BUILDING/APP_STARTING, skip LLM calls and just poll
|
|
|
|
| 1317 |
continue
|
| 1318 |
|
| 1319 |
do_turn("Eve", "Adam", EVE_SPACE)
|
| 1320 |
+
time.sleep(20) # longer pause β each turn does more work now
|
| 1321 |
|
| 1322 |
# Check if we just triggered a build β skip Adam's turn if so
|
| 1323 |
if child_state["stage"] in ("BUILDING", "RESTARTING", "APP_STARTING"):
|
|
|
|
| 1330 |
if len(history) > MAX_HISTORY:
|
| 1331 |
history = history[-MAX_HISTORY:]
|
| 1332 |
|
| 1333 |
+
time.sleep(20) # longer pause β each turn does more work now
|