Merge pull request 'fix: 关 qwen 思考模式(消除 reasoning 回退) + 修 bounce 死锁 (BUG-A/BUG-B)' (#26) from fix/qwen-thinking-and-bounce-deadlock into main
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
+16
-1
@@ -610,9 +610,19 @@ Return ONLY the JSON, no other text."""
|
|||||||
rc = extra.get("reasoning_content") if isinstance(extra, dict) else None
|
rc = extra.get("reasoning_content") if isinstance(extra, dict) else None
|
||||||
return (rc or "").strip()
|
return (rc or "").strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _thinking_extra_body() -> Optional[dict]:
|
||||||
|
"""qwen3.7-max defaults to THINKING mode: on heavy impl subtasks it spends the token
|
||||||
|
budget on `reasoning_content` and never emits the final `content` JSON, so json.loads("")
|
||||||
|
fails ("Expecting value: line 1 column 1"). Disable thinking for deterministic structured
|
||||||
|
execution. Set AGENT_ENABLE_THINKING=1 to opt back in (e.g. for non-structured analysis)."""
|
||||||
|
enabled = os.getenv("AGENT_ENABLE_THINKING", "false").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
return None if enabled else {"enable_thinking": False}
|
||||||
|
|
||||||
async def _complete(self, prompt: str, max_tokens: int) -> str:
|
async def _complete(self, prompt: str, max_tokens: int) -> str:
|
||||||
"""Call the LLM with optional Jina MCP tools; handles the tool-call loop."""
|
"""Call the LLM with optional Jina MCP tools; handles the tool-call loop."""
|
||||||
extra_headers = self._model_attribution_headers()
|
extra_headers = self._model_attribution_headers()
|
||||||
|
extra_body = self._thinking_extra_body()
|
||||||
tools = await self._load_jina_tools()
|
tools = await self._load_jina_tools()
|
||||||
messages = [{"role": "user", "content": prompt}]
|
messages = [{"role": "user", "content": prompt}]
|
||||||
|
|
||||||
@@ -623,6 +633,8 @@ Return ONLY the JSON, no other text."""
|
|||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
extra_headers=extra_headers or None,
|
extra_headers=extra_headers or None,
|
||||||
)
|
)
|
||||||
|
if extra_body:
|
||||||
|
kwargs["extra_body"] = extra_body
|
||||||
if tools:
|
if tools:
|
||||||
kwargs["tools"] = tools
|
kwargs["tools"] = tools
|
||||||
kwargs["tool_choice"] = "auto"
|
kwargs["tool_choice"] = "auto"
|
||||||
@@ -656,10 +668,13 @@ Return ONLY the JSON, no other text."""
|
|||||||
|
|
||||||
# Fallback: ask for a final answer without tools
|
# Fallback: ask for a final answer without tools
|
||||||
messages.append({"role": "user", "content": "Please provide your final answer now."})
|
messages.append({"role": "user", "content": "Please provide your final answer now."})
|
||||||
response = await self.client.chat.completions.create(
|
fallback_kwargs: dict = dict(
|
||||||
model=self.model, messages=messages, max_tokens=max_tokens,
|
model=self.model, messages=messages, max_tokens=max_tokens,
|
||||||
extra_headers=extra_headers or None,
|
extra_headers=extra_headers or None,
|
||||||
)
|
)
|
||||||
|
if extra_body:
|
||||||
|
fallback_kwargs["extra_body"] = extra_body
|
||||||
|
response = await self.client.chat.completions.create(**fallback_kwargs)
|
||||||
self._record_openai_usage(response)
|
self._record_openai_usage(response)
|
||||||
return self._message_text(response.choices[0].message)
|
return self._message_text(response.choices[0].message)
|
||||||
|
|
||||||
|
|||||||
@@ -776,6 +776,14 @@ async def refresh_swarm_run_status(run):
|
|||||||
if collect_generated_files([t]).get("impl"):
|
if collect_generated_files([t]).get("impl"):
|
||||||
if await task_queue.reopen_task(t.task_id):
|
if await task_queue.reopen_task(t.task_id):
|
||||||
reopened += 1
|
reopened += 1
|
||||||
|
# Deadlock guard: when the impl subtask(s) FAILED (0 impl files) and only test tasks
|
||||||
|
# produced output, the loop above finds nothing to reopen → reopens=0 → the run would
|
||||||
|
# spin 'running' forever. Reopen the FAILED tasks so the next cycle retries the impl.
|
||||||
|
if reopened == 0:
|
||||||
|
for t in tasks:
|
||||||
|
if t.status == TaskStatus.FAILED and await task_queue.reopen_task(t.task_id):
|
||||||
|
reopened += 1
|
||||||
|
if reopened > 0:
|
||||||
run.metadata["review_cycles"] = agg_cycles + 1
|
run.metadata["review_cycles"] = agg_cycles + 1
|
||||||
run.metadata["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
|
run.metadata["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
|
||||||
await swarm_runtime.save_run(run)
|
await swarm_runtime.save_run(run)
|
||||||
@@ -783,6 +791,14 @@ async def refresh_swarm_run_status(run):
|
|||||||
run.swarm_id, reopened, agg_cycles + 1,
|
run.swarm_id, reopened, agg_cycles + 1,
|
||||||
(agg.get("validation") or {}).get("pass_rate"))
|
(agg.get("validation") or {}).get("pass_rate"))
|
||||||
return
|
return
|
||||||
|
# Nothing reopenable (no impl files and no failed tasks to retry): bouncing here would
|
||||||
|
# deadlock the run in 'running'. Stop bouncing and accept the existing artifacts so the
|
||||||
|
# run terminates via the normal completion/convergence path below.
|
||||||
|
agg["decision"] = "accept_no_rework"
|
||||||
|
run.metadata["aggregation"] = agg
|
||||||
|
logger.warning("aggregation: bounce on run %s but nothing reopenable (reopens=0) — "
|
||||||
|
"accepting existing artifacts to avoid deadlock (cycle %d)",
|
||||||
|
run.swarm_id, agg_cycles + 1)
|
||||||
await swarm_runtime.save_run(run)
|
await swarm_runtime.save_run(run)
|
||||||
|
|
||||||
if run.status == next_status:
|
if run.status == next_status:
|
||||||
|
|||||||
Reference in New Issue
Block a user