refactor: agent loop done_hooks, inline_eval, scheduler logging & health_check

- agent_loop: for→while, _done_hooks callback mechanism, max_turns=40
- agentmain: pass self to handler, abort clears task queue
- stapp: heartbeat yield for Streamlit StopException detection
- ga: _inline_eval param for in-process eval, no_tool thresholds tuned
- scheduler: logging, max_delay_hours, weekday repeat, health_check()
This commit is contained in:
Liang Jiaqing
2026-03-30 13:51:21 +08:00
parent 7b704b5dbc
commit 473659ba6e
5 changed files with 131 additions and 37 deletions

28
ga.py
View File

@@ -299,7 +299,12 @@ class GenericAgentHandler(BaseHandler):
raw_path = os.path.join(self.cwd, args.get("cwd", './'))
cwd = os.path.normpath(os.path.abspath(raw_path))
code_cwd = os.path.normpath(self.cwd)
result = yield from code_run(code, code_type, timeout, cwd, code_cwd=code_cwd, stop_signal=self.code_stop_signal)
if args.get("_inline_eval"):
ns = {'handler': self, 'parent': self.parent}
try: result = repr(eval(code, ns))
except SyntaxError: exec(code, ns); result = ns.get('_r', 'OK')
except Exception as e: result = f'Error: {e}'
else: result = yield from code_run(code, code_type, timeout, cwd, code_cwd=code_cwd, stop_signal=self.code_stop_signal)
next_prompt = self._get_anchor_prompt()
return StepOutcome(result, next_prompt=next_prompt)
@@ -394,8 +399,7 @@ class GenericAgentHandler(BaseHandler):
with open(path, 'a' if mode == "append" else 'w', encoding="utf-8") as f: f.write(new_content)
yield f"[Status] ✅ {mode.capitalize()} 成功 ({len(new_content)} bytes)\n"
next_prompt = self._get_anchor_prompt()
return StepOutcome({"status": "success", 'writed_bytes': len(new_content)},
next_prompt=next_prompt)
return StepOutcome({"status": "success", 'writed_bytes': len(new_content)}, next_prompt=next_prompt)
except Exception as e:
yield f"[Status] ❌ 写入异常: {str(e)}\n"
return StepOutcome({"status": "error", "msg": str(e)}, next_prompt="\n")
@@ -437,31 +441,29 @@ class GenericAgentHandler(BaseHandler):
def do_no_tool(self, args, response):
'''这是一个特殊工具由引擎自主调用不要包含在TOOLS_SCHEMA里。
当模型在一轮中未显式调用任何工具时,由引擎自动触发。
二次确认仅在回复几乎只包含<thinking>/<summary>和一段大代码块时触发。
'''
二次确认仅在回复几乎只包含<thinking>/<summary>和一段大代码块时触发。'''
content = getattr(response, 'content', '') or ""
if not response or not content.strip():
yield "[Warn] LLM returned an empty response. Retrying...\n"
return StepOutcome({}, next_prompt="[System] Blank response, regenerate and tooluse", should_exit=False)
return StepOutcome({}, next_prompt="[System] Blank response, regenerate and tooluse")
if '流异常中断,未收到完整响应 !!!]' in content:
return StepOutcome({}, next_prompt="[System] Incomplete response. Regenerate and tooluse.", should_exit=False)
return StepOutcome({}, next_prompt="[System] Incomplete response. Regenerate and tooluse.")
if 'max_tokens !!!]' in content:
return StepOutcome({}, next_prompt="[System] max_tokens limit reached. Use multi small steps to do it.", should_exit=False)
return StepOutcome({}, next_prompt="[System] max_tokens limit reached. Use multi small steps to do it.")
# 2. 检测“包含较大代码块但未调用工具”的情况
# 这里通过三引号代码块 + 最少字符数的方式粗略判断“大段代码”
code_block_pattern = r"```[a-zA-Z0-9_]*\n[\s\S]{100,}?```"
code_block_pattern = r"```[a-zA-Z0-9_]*\n[\s\S]{300,}?```"
m = re.search(code_block_pattern, content)
if m:
# 仅当 content 由 <thinking> / <summary> 和该代码块构成时才触发二次确认
residual = content
# 去掉代码块本身
residual = residual.replace(m.group(0), "")
# 去掉<thinking>和<summary>块(大小写不敏感)
residual = re.sub(r"<thinking>[\s\S]*?</thinking>", "", residual, flags=re.IGNORECASE)
residual = re.sub(r"<summary>[\s\S]*?</summary>", "", residual, flags=re.IGNORECASE)
# 如果去除上述结构后的非空白字符很少,说明没有额外自然语言说明
clean_residual = re.sub(r"\s+", "", residual)
if len(clean_residual) <= 50:
if len(clean_residual) <= 20:
yield "[Info] Detected large code block without tool call and no extra natural language. Requesting clarification.\n"
next_prompt = (
"[System] 检测到你在上一轮回复中主要内容是较大代码块(仅配有<thinking>/<summary>),且本轮未调用任何工具。\n"
@@ -470,10 +472,10 @@ class GenericAgentHandler(BaseHandler):
"如果只是向用户展示或讲解代码片段,请在回复中补充自然语言说明,"
"并明确是否还需要额外的实际操作。"
)
return StepOutcome({}, next_prompt=next_prompt, should_exit=False)
return StepOutcome({}, next_prompt=next_prompt)
# 3. 正常情况:直接将回复返回给用户并结束循环
yield "[Info] Final response to user.\n"
return StepOutcome(response, next_prompt=None, should_exit=True)
return StepOutcome(response, next_prompt=None)
def do_start_long_term_update(self, args, response):
'''Agent觉得当前任务完成后有重要信息需要记忆时调用此工具。'''