tao-shen Claude Opus 4.6 commited on
Commit
aaf4be2
·
1 Parent(s): 4d022f6

fix: improve agent mechanism — error details, loop prevention, config safety

Browse files

- check_health now returns actual HF runtime errorMessage
- Global action history tracks all actions across turns
- Smarter guidance prevents read_file loops (after 3 reads → force write)
- Safety filter strips invalid OpenClaw config keys before writing
- Better progression: check → read → write → verify cycle

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

Files changed (1) hide show
  1. scripts/conversation-loop.py +83 -19
scripts/conversation-loop.py CHANGED
@@ -171,10 +171,26 @@ def action_check_health():
171
  child_state["stage"] = stage
172
  child_state["alive"] = (stage == "RUNNING")
173
  if stage in ("RUNTIME_ERROR", "BUILD_ERROR"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  return (f"{CHILD_NAME} has a {stage}! "
175
- f"To diagnose: read the Dockerfile [ACTION: read_file:space:Dockerfile], "
176
- f"check scripts [ACTION: read_file:space:scripts/token-redirect.cjs], "
177
- f"or try [ACTION: restart] to see if it self-heals.")
 
178
  return f"{CHILD_NAME} stage: {stage}. {'Running but API not responding.' if stage == 'RUNNING' else ''}"
179
  except Exception as e:
180
  return f"Cannot reach {CHILD_NAME}: {e}"
@@ -224,6 +240,27 @@ def action_write_file(target, path, content):
224
  """Write a file to the child's Space or Dataset."""
225
  repo_type = "space" if target == "space" else "dataset"
226
  repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  try:
228
  hf_api.upload_file(
229
  path_or_fileobj=io.BytesIO(content.encode()),
@@ -433,6 +470,8 @@ def set_bubble(url, text_en, text_zh=""):
433
  history = []
434
  MAX_HISTORY = 24
435
  last_action_results = []
 
 
436
 
437
 
438
  def get_child_status():
@@ -512,27 +551,48 @@ def build_user_prompt(speaker, other):
512
  for ar in last_action_results:
513
  action_context += f" [{ar['action']}]:\n{ar['result']}\n"
514
 
515
- # Guidance based on state — prevent action loops
516
  guidance = ""
517
  recent_actions = [ar["action"].split(":")[0] for ar in last_action_results] if last_action_results else []
 
 
 
 
 
 
 
 
 
518
  if not child_state["created"]:
519
  guidance = "Your child hasn't been born yet. Use [ACTION: create_child] now!"
520
- elif "check_health" in recent_actions or "list_files" in recent_actions:
521
- guidance = ("You already checked health/listed files. Now READ a specific file to understand the problem. "
522
- "Try [ACTION: read_file:space:Dockerfile] or [ACTION: read_file:space:scripts/token-redirect.cjs] "
523
- "or [ACTION: read_file:dataset:.openclaw/openclaw.json]. "
524
- "DO NOT repeat check_health or list_files move forward!")
525
- elif "read_file" in recent_actions:
526
- guidance = ("You've read a file. Now DECIDE: what needs to change? "
527
- "Use [ACTION: write_file:space:PATH] or [ACTION: write_file:dataset:PATH] to make improvements, "
528
- "or [ACTION: restart] if you need to apply changes.")
529
  elif "write_file" in recent_actions:
530
- guidance = ("You just modified a file. If it was a Space file, it triggers a rebuild. "
531
- "Use [ACTION: check_health] to see if Cain is recovering, or [ACTION: restart] to apply changes.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  else:
533
- guidance = ("Explore your child. Use [ACTION: read_file:space:Dockerfile] to see the build, "
534
- "or [ACTION: read_file:dataset:.openclaw/openclaw.json] to see the config. "
535
- "Don't just check_health repeatedly — dig into the actual files!")
536
 
537
  return f"""You are {speaker}, talking with {other}.
538
 
@@ -548,7 +608,8 @@ If you take an action, put [ACTION: ...] after your dialogue, before the --- sep
548
 
549
  def do_turn(speaker, other, space_url):
550
  """Execute one conversation turn with potential actions."""
551
- global last_action_results
 
552
 
553
  system = build_system_prompt()
554
  user = build_user_prompt(speaker, other)
@@ -561,6 +622,9 @@ def do_turn(speaker, other, space_url):
561
  # Parse and execute any actions
562
  clean_text, action_results = parse_and_execute_actions(raw_reply)
563
  last_action_results = action_results
 
 
 
564
 
565
  # Parse bilingual
566
  en, zh = parse_bilingual(clean_text)
 
171
  child_state["stage"] = stage
172
  child_state["alive"] = (stage == "RUNNING")
173
  if stage in ("RUNTIME_ERROR", "BUILD_ERROR"):
174
+ # Get actual error message from runtime API
175
+ error_detail = ""
176
+ try:
177
+ rresp = requests.get(
178
+ f"https://huggingface.co/api/spaces/{CHILD_SPACE_ID}/runtime",
179
+ headers={"Authorization": f"Bearer {HF_TOKEN}"}, timeout=10)
180
+ if rresp.ok:
181
+ rdata = rresp.json()
182
+ error_detail = rdata.get("errorMessage", "")
183
+ # Extract just the key error lines
184
+ if error_detail:
185
+ lines = [l.strip() for l in error_detail.split('\n') if l.strip() and '│' not in l]
186
+ error_detail = " | ".join(lines[-5:]) # Last 5 meaningful lines
187
+ except:
188
+ pass
189
  return (f"{CHILD_NAME} has a {stage}! "
190
+ f"Error: {error_detail or 'unknown'}. "
191
+ f"Options: [ACTION: restart] or fix the config with [ACTION: write_file:dataset:.openclaw/openclaw.json]")
192
+ if stage in ("BUILDING", "STARTING", "APP_STARTING"):
193
+ return f"{CHILD_NAME} is starting up (stage: {stage}). Be patient."
194
  return f"{CHILD_NAME} stage: {stage}. {'Running but API not responding.' if stage == 'RUNNING' else ''}"
195
  except Exception as e:
196
  return f"Cannot reach {CHILD_NAME}: {e}"
 
240
  """Write a file to the child's Space or Dataset."""
241
  repo_type = "space" if target == "space" else "dataset"
242
  repo_id = CHILD_SPACE_ID if target == "space" else CHILD_DATASET_ID
243
+
244
+ # Safety: validate openclaw.json before writing
245
+ if path.endswith("openclaw.json"):
246
+ try:
247
+ cfg = json.loads(content)
248
+ # Remove keys known to cause RUNTIME_ERROR in OpenClaw
249
+ invalid_keys = ["agent", "auth.defaultScope", "gateway.auth.scope"]
250
+ removed = []
251
+ for k in invalid_keys:
252
+ if k in cfg:
253
+ del cfg[k]
254
+ removed.append(k)
255
+ if "models" in cfg and "defaultModel" in cfg["models"]:
256
+ del cfg["models"]["defaultModel"]
257
+ removed.append("models.defaultModel")
258
+ if removed:
259
+ content = json.dumps(cfg, indent=2)
260
+ print(f"[SAFETY] Removed invalid config keys: {removed}")
261
+ except json.JSONDecodeError:
262
+ return f"Error: invalid JSON in config file. Please fix the content."
263
+
264
  try:
265
  hf_api.upload_file(
266
  path_or_fileobj=io.BytesIO(content.encode()),
 
470
  history = []
471
  MAX_HISTORY = 24
472
  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():
 
551
  for ar in last_action_results:
552
  action_context += f" [{ar['action']}]:\n{ar['result']}\n"
553
 
554
+ # Guidance based on global action history — prevent loops, push toward progress
555
  guidance = ""
556
  recent_actions = [ar["action"].split(":")[0] for ar in last_action_results] if last_action_results else []
557
+
558
+ # Count action types in last 6 actions globally
559
+ recent_global = action_history[-6:] if action_history else []
560
+ global_action_names = [a["action"].split(":")[0] for a in recent_global]
561
+ read_count = global_action_names.count("read_file")
562
+ check_count = global_action_names.count("check_health")
563
+ list_count = global_action_names.count("list_files")
564
+ write_count = global_action_names.count("write_file")
565
+
566
  if not child_state["created"]:
567
  guidance = "Your child hasn't been born yet. Use [ACTION: create_child] now!"
568
+ elif check_count + list_count >= 3 and write_count == 0 and read_count == 0:
569
+ guidance = ("STOP checking health and listing files repeatedly! "
570
+ "READ a specific file: [ACTION: read_file:space:Dockerfile] or "
571
+ "[ACTION: read_file:dataset:.openclaw/openclaw.json]")
572
+ elif read_count >= 3 and write_count == 0:
573
+ guidance = ("You've read enough files. It's time to ACT! "
574
+ "DECIDE what to change and use [ACTION: write_file:...] to make an improvement, "
575
+ "or use [ACTION: restart] to restart Cain. Stop reading and START improving!")
 
576
  elif "write_file" in recent_actions:
577
+ guidance = ("You just modified a file. Good! Now verify: "
578
+ "use [ACTION: check_health] to see if Cain is recovering, "
579
+ "or [ACTION: restart] to apply changes.")
580
+ elif "restart" in recent_actions:
581
+ guidance = ("You restarted Cain. Wait a moment, then [ACTION: check_health] to see the result.")
582
+ elif "check_health" in recent_actions and child_state["stage"] in ("RUNTIME_ERROR", "BUILD_ERROR"):
583
+ guidance = ("Cain has an error! Read the config [ACTION: read_file:dataset:.openclaw/openclaw.json] "
584
+ "or try [ACTION: restart]. Don't just check_health again — take action to fix it!")
585
+ elif "check_health" in recent_actions and child_state["alive"]:
586
+ guidance = ("Cain is healthy! Think about improvements: "
587
+ "read a file to understand it, then write an improved version. "
588
+ "Or [ACTION: send_bubble:Hello Cain!] to communicate with your child.")
589
+ elif "read_file" in recent_actions:
590
+ guidance = ("You've read a file. Now DECIDE what to change and use "
591
+ "[ACTION: write_file:space:PATH] or [ACTION: write_file:dataset:PATH] to improve it. "
592
+ "Or discuss with your partner what you learned.")
593
  else:
594
+ guidance = ("Explore your child: [ACTION: read_file:space:Dockerfile] to see the build, "
595
+ "or [ACTION: read_file:dataset:.openclaw/openclaw.json] for config.")
 
596
 
597
  return f"""You are {speaker}, talking with {other}.
598
 
 
608
 
609
  def do_turn(speaker, other, space_url):
610
  """Execute one conversation turn with potential actions."""
611
+ global last_action_results, turn_count
612
+ turn_count += 1
613
 
614
  system = build_system_prompt()
615
  user = build_user_prompt(speaker, other)
 
622
  # Parse and execute any actions
623
  clean_text, action_results = parse_and_execute_actions(raw_reply)
624
  last_action_results = action_results
625
+ for ar in action_results:
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)