"""Antigravity CLI headless backend. `agy -p` subprocess + 드라이버 스키마 검증·재시도.""" import asyncio import json import re import tempfile from pydantic import BaseModel, ValidationError from deep_research.backends.base import AgentBackend _FENCE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL) def extract_json_block(text: str) -> dict | None: """출력 텍스트에서 JSON 객체 추출: 펜스 우선, 없으면 최외곽 중괄호.""" m = _FENCE.search(text) candidates = [m.group(1)] if m else [] if not candidates: start, depth = -1, 0 for i, ch in enumerate(text): if ch == "{": if depth == 0: start = i depth += 1 elif ch == "}" and depth > 0: depth -= 1 if depth == 0 and start >= 0: candidates.append(text[start:i + 1]) break for c in candidates: try: return json.loads(c) except json.JSONDecodeError: continue return None class AntigravityBackend(AgentBackend): def __init__(self, *, retries: int = 2, neutral_cwd: str | None = None, model: str | None = None): self.retries = retries self.neutral_cwd = neutral_cwd or tempfile.gettempdir() self.model = model async def _invoke(self, prompt: str) -> str | None: # 실 CLI 스모크 (2026-06-10): agy 에 `--cd`/`-m` 플래그 없음 ("flags provided but # not defined" 즉사) — cwd 는 subprocess 인자로, 모델은 `--model`. stdin 차단(비-tty 안전). cmd = ["agy", "-p", prompt] if self.model: cmd += ["--model", self.model] proc = await asyncio.create_subprocess_exec( *cmd, cwd=self.neutral_cwd, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) out, _ = await proc.communicate() if proc.returncode != 0: return None return out.decode("utf-8", "replace") async def run_agent(self, prompt: str, schema: type[BaseModel], *, label: str): schema_json = json.dumps(schema.model_json_schema()) full = (prompt + "\n\n## Output format\nReturn ONLY a JSON object conforming to this " "JSON Schema (no prose, no markdown fences):\n" + schema_json) for attempt in range(self.retries + 1): text = await self._invoke(full if attempt == 0 else full + "\n\nYour previous output was invalid. Return ONLY the JSON object.") if text is None: continue obj = extract_json_block(text) if obj is None: continue try: return schema.model_validate(obj) except ValidationError: continue return None