|
|
@@ -378,12 +378,23 @@ async def run_agent(
|
|
|
# kwarg-pass below doesn't NameError.
|
|
|
continue_workspace = ""
|
|
|
|
|
|
- # Execute via lambdagent
|
|
|
+ # Execute via lambdagent.
|
|
|
+ # audit #10: _execute_agent is sync and runs multi-step LLM/tool work
|
|
|
+ # for seconds-to-minutes; calling it directly from this async handler
|
|
|
+ # freezes the entire FastAPI event loop (all health checks, status
|
|
|
+ # polls, /cancel endpoints stall). Offload to the default thread pool.
|
|
|
+ # The streaming variant (~line 671) already uses a daemon thread; this
|
|
|
+ # brings the sync /run path in line.
|
|
|
+ import asyncio as _asyncio
|
|
|
t0 = time.time()
|
|
|
try:
|
|
|
- result, trace_info = _execute_agent(
|
|
|
- config, enriched_input, agent_dir=agent_dir, run_id=run_id,
|
|
|
- continue_workspace=continue_workspace,
|
|
|
+ _loop = _asyncio.get_event_loop()
|
|
|
+ result, trace_info = await _loop.run_in_executor(
|
|
|
+ None,
|
|
|
+ lambda: _execute_agent(
|
|
|
+ config, enriched_input, agent_dir=agent_dir, run_id=run_id,
|
|
|
+ continue_workspace=continue_workspace,
|
|
|
+ ),
|
|
|
)
|
|
|
duration_ms = int((time.time() - t0) * 1000)
|
|
|
workspace_path = trace_info.get("workspace_path", "")
|
|
|
@@ -546,13 +557,12 @@ async def run_agent_stream(
|
|
|
f"[本次新指令]\n{stream_input}"
|
|
|
)
|
|
|
|
|
|
- # The 18-step enforceLoop floor is for fresh runs only — drop
|
|
|
- # it on continues so a "fix the references" follow-up doesn't
|
|
|
- # force a full pipeline rerun.
|
|
|
- if isinstance(config.get("react"), dict) and config["react"].get("enforceLoop"):
|
|
|
- config["react"] = {**config["react"]}
|
|
|
- config["react"].pop("enforceLoop", None)
|
|
|
- logger.info("continue-run: enforceLoop disabled for follow-up turn")
|
|
|
+ # NOTE: enforceLoop is now mode-aware (see mode block below).
|
|
|
+ # Previously we unconditionally dropped it on every continue,
|
|
|
+ # which made `mode=iterate` silently behave like edit/chat —
|
|
|
+ # the orchestrator was terminating after one sub-agent attempt
|
|
|
+ # because no enforceLoop floor was holding it to the full
|
|
|
+ # pipeline. iterate now keeps the floor; chat / edit drop it.
|
|
|
else:
|
|
|
logger.warning("continue-run prev run not found or has no workspace: %s", ctx_run_id)
|
|
|
|
|
|
@@ -693,22 +703,57 @@ async def run_agent_stream(
|
|
|
"[ProviderError]",
|
|
|
"[ERROR]",
|
|
|
)
|
|
|
- output_is_error = any(
|
|
|
- final.lstrip().startswith(p) for p in _ERROR_PREFIXES
|
|
|
+ # Substring fail markers — for cases where the orchestrator
|
|
|
+ # composes a natural-language failure report (no machine
|
|
|
+ # prefix). Catches "Sub-agent 调用全部超时", "系统工具链存在
|
|
|
+ # 问题", "无法完成", "未能完成", "all sub-agents failed",
|
|
|
+ # etc. Conservative: only fires when paired with the
|
|
|
+ # `not made_progress` check below.
|
|
|
+ _FAILURE_KEYWORDS = (
|
|
|
+ "全部超时", "全部 timeout", "全部失败",
|
|
|
+ "无法完成", "未能完成", "无法继续",
|
|
|
+ "系统工具链存在问题",
|
|
|
+ "all sub-agents failed", "all calls failed",
|
|
|
+ "unable to complete",
|
|
|
+ )
|
|
|
+ output_is_error = (
|
|
|
+ any(final.lstrip().startswith(p) for p in _ERROR_PREFIXES)
|
|
|
+ or any(kw in final for kw in _FAILURE_KEYWORDS)
|
|
|
)
|
|
|
# Scan trace entries for evidence of any successful tool
|
|
|
# call ("[Step K] X done" — see _compress_state in the
|
|
|
- # engine). If even one tool completed, we treat the run
|
|
|
- # as partial success and keep status='completed'.
|
|
|
+ # engine). The engine writes "done" markers even when the
|
|
|
+ # tool returned an error (e.g. STALL), so we must also
|
|
|
+ # inspect each entry's observation to filter those out.
|
|
|
+ # Otherwise an all-error run gets status=completed because
|
|
|
+ # the trace technically has "[Step 1] call_physreview done"
|
|
|
+ # markers despite physreview producing only [CLAUDE-CODE_ERROR].
|
|
|
trace_json = trace_info.get("trace_json", "[]")
|
|
|
made_progress = False
|
|
|
+ # Markers that prove a tool's observation was an error rather
|
|
|
+ # than real output. Kept in sync with compiler._observation_failed.
|
|
|
+ _FAIL_MARKERS = (
|
|
|
+ "[tool_error]", "[validation_error]", "[claude code error]",
|
|
|
+ "[claude-code_error]", "[error]", "[mcp_error", "[mcp_timeout",
|
|
|
+ " timed out", " timeout", " not found", " compile error",
|
|
|
+ '"status":"fail"', '"status": "fail"',
|
|
|
+ '"ok":false', '"ok": false',
|
|
|
+ )
|
|
|
try:
|
|
|
import re as _re
|
|
|
for entry in json.loads(trace_json) or []:
|
|
|
out = str(entry.get("output", ""))
|
|
|
- if _re.search(r"\[Step \d+\] [^\n]*\bdone\b", out):
|
|
|
- made_progress = True
|
|
|
- break
|
|
|
+ if not _re.search(r"\[Step \d+\] [^\n]*\bdone\b", out):
|
|
|
+ continue
|
|
|
+ # A done marker exists; check the observation in the
|
|
|
+ # same entry to see if the tool actually succeeded.
|
|
|
+ # The engine concatenates observation into the state
|
|
|
+ # output, so we look at the whole entry text.
|
|
|
+ out_low = out.lower()
|
|
|
+ if any(m in out_low for m in _FAIL_MARKERS):
|
|
|
+ continue # done marker but content is an error
|
|
|
+ made_progress = True
|
|
|
+ break
|
|
|
except Exception:
|
|
|
pass
|
|
|
|