Internal method to query Claude and stream responses. Handles tool calls (Write) and text responses. Supports multimodal content with image attachments. IMPORTANT: Spec creation requires BOTH files to be written: 1. app_spec.txt - the main specification
(
self,
message: str,
attachments: list[ImageAttachment] | None = None
)
| 245 | } |
| 246 | |
| 247 | async def _query_claude( |
| 248 | self, |
| 249 | message: str, |
| 250 | attachments: list[ImageAttachment] | None = None |
| 251 | ) -> AsyncGenerator[dict, None]: |
| 252 | """ |
| 253 | Internal method to query Claude and stream responses. |
| 254 | |
| 255 | Handles tool calls (Write) and text responses. |
| 256 | Supports multimodal content with image attachments. |
| 257 | |
| 258 | IMPORTANT: Spec creation requires BOTH files to be written: |
| 259 | 1. app_spec.txt - the main specification |
| 260 | 2. initializer_prompt.md - tells the agent how many features to create |
| 261 | |
| 262 | We only signal spec_complete when BOTH files are verified on disk. |
| 263 | """ |
| 264 | if not self.client: |
| 265 | return |
| 266 | |
| 267 | # Build the message content |
| 268 | if attachments and len(attachments) > 0: |
| 269 | # Multimodal message: build content blocks array |
| 270 | content_blocks: list[dict[str, Any]] = [] |
| 271 | |
| 272 | # Add text block if there's text |
| 273 | if message: |
| 274 | content_blocks.append({"type": "text", "text": message}) |
| 275 | |
| 276 | # Add image blocks |
| 277 | for att in attachments: |
| 278 | content_blocks.append({ |
| 279 | "type": "image", |
| 280 | "source": { |
| 281 | "type": "base64", |
| 282 | "media_type": att.mimeType, |
| 283 | "data": att.base64Data, |
| 284 | } |
| 285 | }) |
| 286 | |
| 287 | # Send multimodal content to Claude using async generator format |
| 288 | # The SDK's query() accepts AsyncIterable[dict] for custom message formats |
| 289 | await self.client.query(make_multimodal_message(content_blocks)) |
| 290 | logger.info(f"Sent multimodal message with {len(attachments)} image(s)") |
| 291 | else: |
| 292 | # Text-only message: use string format |
| 293 | await self.client.query(message) |
| 294 | |
| 295 | current_text = "" |
| 296 | |
| 297 | # Track pending writes for BOTH required files |
| 298 | pending_writes: dict[str, dict[str, Any] | None] = { |
| 299 | "app_spec": None, # {"tool_id": ..., "path": ...} |
| 300 | "initializer": None, # {"tool_id": ..., "path": ...} |
| 301 | } |
| 302 | |
| 303 | # Track which files have been successfully written |
| 304 | files_written = { |
no test coverage detected