Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
435f720cfb | ||
|
|
85e6ea6dfb | ||
|
|
cdb17a0d73 | ||
|
|
b0831645cf | ||
|
|
5cd266ae17 | ||
|
|
fadca8f4fc |
+73
-2
@@ -11,6 +11,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
@@ -377,6 +378,7 @@ Return ONLY the JSON array, no other text."""
|
||||
task_id, context.get("specialist_role", "general"), description)
|
||||
workspace_files = self._summarize_workspace()
|
||||
workspace_context = self._collect_workspace_context()
|
||||
exact_target_files = self._load_exact_target_files(description)
|
||||
user_prompt = context.get("user_prompt") or context.get("run_goal") or context.get("root_task_description") or ""
|
||||
specialist_role = context.get("specialist_role", "general")
|
||||
dependency_artifacts = context.get("dependency_artifacts") or []
|
||||
@@ -447,6 +449,16 @@ Current workspace files:
|
||||
{json.dumps(workspace_files, indent=2)}
|
||||
Relevant workspace file contents:
|
||||
{json.dumps(workspace_context, indent=2)}
|
||||
|
||||
EXACT CURRENT CONTENT of the file(s) this task edits (verbatim from disk — authoritative):
|
||||
{json.dumps(exact_target_files, indent=2)}
|
||||
^ When a file appears above, your returned `content` for it MUST be that exact text with ONLY the
|
||||
minimal edits the task requires. Copy every other line byte-for-byte: imports, comments, copyright
|
||||
headers, blank lines, whitespace, and unrelated code. Do NOT reformat, reorder, rename, restyle,
|
||||
"clean up", or re-wrap anything the task does not explicitly require. The resulting diff against the
|
||||
base commit must contain ONLY the lines the task needs — unrelated changes break tests and fail the
|
||||
patch. If a file you must edit is NOT shown above, read it conceptually from the listed workspace
|
||||
contents and still change as little as possible.
|
||||
Peer specialist input:
|
||||
{json.dumps(peer_context, indent=2)}
|
||||
|
||||
@@ -457,7 +469,9 @@ Workspace & tool boundaries (IMPORTANT):
|
||||
it is not listed above: if the task references e.g. `qutebrowser/utils/log.py`, that file EXISTS
|
||||
on disk at {self.workspace_dir}/qutebrowser/utils/log.py — assume it is present and edit it.
|
||||
- Treat the local files as the single source of truth. Return the complete modified content of each
|
||||
file you change in `files` (full file, not a diff).
|
||||
file you change in `files` (full file, not a diff) — but it MUST be the file's exact current content
|
||||
with only the minimal task-required edits applied (see "EXACT CURRENT CONTENT" above). Preserve all
|
||||
unchanged lines verbatim; never regenerate a file from memory or reformat it.
|
||||
- The web search / read_url tools are for EXTERNAL knowledge ONLY (library/framework docs, language
|
||||
features, error-message lookups). NEVER use them to fetch THIS repository's own source — the web
|
||||
copy is a DIFFERENT version than your local base commit and will corrupt your patch.
|
||||
@@ -610,9 +624,19 @@ Return ONLY the JSON, no other text."""
|
||||
rc = extra.get("reasoning_content") if isinstance(extra, dict) else None
|
||||
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:
|
||||
"""Call the LLM with optional Jina MCP tools; handles the tool-call loop."""
|
||||
extra_headers = self._model_attribution_headers()
|
||||
extra_body = self._thinking_extra_body()
|
||||
tools = await self._load_jina_tools()
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
@@ -623,6 +647,8 @@ Return ONLY the JSON, no other text."""
|
||||
max_tokens=max_tokens,
|
||||
extra_headers=extra_headers or None,
|
||||
)
|
||||
if extra_body:
|
||||
kwargs["extra_body"] = extra_body
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
kwargs["tool_choice"] = "auto"
|
||||
@@ -656,10 +682,13 @@ Return ONLY the JSON, no other text."""
|
||||
|
||||
# Fallback: ask for a final answer without tools
|
||||
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,
|
||||
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)
|
||||
return self._message_text(response.choices[0].message)
|
||||
|
||||
@@ -837,6 +866,48 @@ Return ONLY JSON:
|
||||
logger.warning(f"Failed to read workspace file {relative}: {e}")
|
||||
return context
|
||||
|
||||
#: Path-like tokens in a subtask description, e.g. `lib/ansible/module_utils/urls.py`.
|
||||
_PATH_RE = re.compile(
|
||||
r"[A-Za-z0-9_][A-Za-z0-9_./\-]*\.(?:py|js|jsx|ts|tsx|go|rb|java|c|h|cc|cpp|hpp|"
|
||||
r"rs|php|cs|kt|swift|scala|vue|json|ya?ml|toml|cfg|ini|sh)\b"
|
||||
)
|
||||
|
||||
def _load_exact_target_files(self, description: str, max_files: int = 6,
|
||||
max_bytes: int = 120000) -> dict[str, str]:
|
||||
"""Load the FULL, UNTRUNCATED on-disk content of the file(s) a subtask names.
|
||||
|
||||
Root cause of noisy/non-applying patches: the agent was only shown a truncated SAMPLE of the
|
||||
repo, so it reconstructed whole files from partial input → dropped/reordered/reformatted lines
|
||||
→ huge diffs that break tests. Here we read the exact current content of the files referenced
|
||||
in the subtask description so the model can preserve every unchanged line verbatim and edit
|
||||
only what the task requires. Paths that don't resolve inside the workspace are skipped."""
|
||||
if not description:
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
# Longest paths first: prefer `a/b/c.py` over a bare `c.py` mention of the same file.
|
||||
cands = sorted(set(m.group(0) for m in self._PATH_RE.finditer(description)),
|
||||
key=len, reverse=True)
|
||||
for cand in cands:
|
||||
if len(out) >= max_files:
|
||||
break
|
||||
try:
|
||||
p = self._resolve_workspace_path(cand)
|
||||
except ValueError:
|
||||
continue
|
||||
if not p.is_file():
|
||||
continue
|
||||
try:
|
||||
data = p.read_bytes()
|
||||
except Exception:
|
||||
continue
|
||||
if len(data) > max_bytes:
|
||||
continue # too large to inject in full; leave to the sampled context
|
||||
try:
|
||||
out[cand] = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return out
|
||||
|
||||
def _resolve_workspace_path(self, file_path: str) -> Path:
|
||||
if not file_path or os.path.isabs(file_path):
|
||||
raise ValueError(f"Invalid relative path: {file_path}")
|
||||
|
||||
+23
-7
@@ -776,13 +776,29 @@ async def refresh_swarm_run_status(run):
|
||||
if collect_generated_files([t]).get("impl"):
|
||||
if await task_queue.reopen_task(t.task_id):
|
||||
reopened += 1
|
||||
run.metadata["review_cycles"] = agg_cycles + 1
|
||||
run.metadata["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
|
||||
await swarm_runtime.save_run(run)
|
||||
logger.info("aggregation: bounced run %s — reopened %d for rework (cycle %d, pass_rate=%s)",
|
||||
run.swarm_id, reopened, agg_cycles + 1,
|
||||
(agg.get("validation") or {}).get("pass_rate"))
|
||||
return
|
||||
# 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["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
|
||||
await swarm_runtime.save_run(run)
|
||||
logger.info("aggregation: bounced run %s — reopened %d for rework (cycle %d, pass_rate=%s)",
|
||||
run.swarm_id, reopened, agg_cycles + 1,
|
||||
(agg.get("validation") or {}).get("pass_rate"))
|
||||
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)
|
||||
|
||||
if run.status == next_status:
|
||||
|
||||
Reference in New Issue
Block a user