Routes LLM operations through the claude / codex CLI subscription.
| 121 | |
| 122 | |
| 123 | class CawBackend(LLMBackend): |
| 124 | """Routes LLM operations through the claude / codex CLI subscription.""" |
| 125 | |
| 126 | def __init__(self, config: Config) -> None: |
| 127 | self._config = config |
| 128 | self._caw_provider = _resolve_caw_provider(config.provider) |
| 129 | # main_model is passed straight through; empty string → caw default. |
| 130 | self._model: str | None = config.main_model or None |
| 131 | |
| 132 | # Fail loudly here rather than producing a confusing caw error mid-run. |
| 133 | cli = _CLI_BINARY[config.provider] |
| 134 | if shutil.which(cli) is None: |
| 135 | raise RuntimeError( |
| 136 | f"Subscription mode requires the '{cli}' CLI on PATH. " |
| 137 | f"Install it and run '{cli} login', then try again." |
| 138 | ) |
| 139 | |
| 140 | if self._caw_provider == "claude_code": |
| 141 | # Prevent claude-code CLI from cancelling long sub-module recursion; |
| 142 | # setdefault preserves a user-supplied value (e.g. shell override). |
| 143 | os.environ.setdefault("MCP_TOOL_TIMEOUT", "86400000") |
| 144 | os.environ.setdefault("MCP_TIMEOUT", "60000") |
| 145 | logger.info( |
| 146 | "claude-code MCP timeouts: MCP_TOOL_TIMEOUT=%s MCP_TIMEOUT=%s", |
| 147 | os.environ["MCP_TOOL_TIMEOUT"], |
| 148 | os.environ["MCP_TIMEOUT"], |
| 149 | ) |
| 150 | |
| 151 | # ------------------------------------------------------------------ |
| 152 | # Single-shot completion (clustering, parent / repo overviews) |
| 153 | # ------------------------------------------------------------------ |
| 154 | |
| 155 | def complete( |
| 156 | self, |
| 157 | prompt: str, |
| 158 | *, |
| 159 | model: str | None = None, |
| 160 | temperature: float = 0.0, # unused: subscription CLIs don't expose temperature |
| 161 | ) -> str: |
| 162 | # Blocks the calling thread for the lifetime of the claude/codex |
| 163 | # subprocess. Callers running this from an async context (e.g. the |
| 164 | # documentation_generator) accept this — there is no concurrent work |
| 165 | # to do while clustering is in flight anyway. |
| 166 | effective_model = model or self._model |
| 167 | agent = CawAgent( |
| 168 | provider=self._caw_provider, |
| 169 | model=effective_model, |
| 170 | tools=ToolGroup.READER, |
| 171 | ) |
| 172 | traj = agent.completion(prompt) |
| 173 | return traj.result |
| 174 | |
| 175 | # ------------------------------------------------------------------ |
| 176 | # Per-module agent loop |
| 177 | # ------------------------------------------------------------------ |
| 178 | |
| 179 | async def run_module_agent( |
| 180 | self, |