Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@
"Skill(bd-issue-tracking)",
"Bash(NODE_OPTIONS=\"--max-old-space-size=4096\" timeout 60 npm test:*)",
"Bash(export NODE_OPTIONS=\"--max-old-space-size=4096\")",
"Bash(/dev/null echo echo '=== Dashboard sub-components (potential candidates) ===' ls /home/frankbria/projects/codeframe/web-ui/src/components/)"
"Bash(/dev/null echo echo '=== Dashboard sub-components (potential candidates) ===' ls /home/frankbria/projects/codeframe/web-ui/src/components/)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove malformed/debug command.

Line 136 contains what appears to be a debug artifact or concatenated shell commands with a hardcoded absolute path. This does not follow the pattern of other entries and should be removed.

-      "Bash(/dev/null echo echo '=== Dashboard sub-components (potential candidates) ===' ls /home/frankbria/projects/codeframe/web-ui/src/components/)",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Bash(/dev/null echo echo '=== Dashboard sub-components (potential candidates) ===' ls /home/frankbria/projects/codeframe/web-ui/src/components/)",
🤖 Prompt for AI Agents
In .claude/settings.local.json around line 136, remove the malformed debug entry
that contains a concatenated shell command and hardcoded path ("Bash(/dev/null
echo echo '=== Dashboard sub-components (potential candidates) ===' ls
/home/frankbria/projects/codeframe/web-ui/src/components/)") so the file
conforms to the pattern of other entries; simply delete this line (or replace it
with a valid setting if intended) and ensure trailing commas and JSON structure
remain valid after removal.

"Bash(git rm:*)"
],
"deny": [],
"ask": []
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **BREAKING**: Converted worker agents to async/await pattern (cf-48)
- `BackendWorkerAgent.execute_task()` is now async
- `FrontendWorkerAgent.execute_task()` is now async
- `TestWorkerAgent.execute_task()` is now async
- All internal agent methods now use async/await
- Replaced `Anthropic` client with `AsyncAnthropic`
- Removed `_broadcast_async()` threading wrapper from all worker agents
- `LeadAgent` now calls worker agents directly with `await` (removed `run_in_executor()`)

### Fixed
- Resolved event loop deadlocks in worker agent broadcasts
- Eliminated threading overhead in agent task execution
- Improved WebSocket broadcast reliability with direct async calls

### Technical Details
- **Files Modified**:
- `codeframe/agents/backend_worker_agent.py`: Full async conversion
- `codeframe/agents/frontend_worker_agent.py`: Full async conversion
- `codeframe/agents/test_worker_agent.py`: Full async conversion
- `codeframe/agents/lead_agent.py`: Removed threading wrapper
- **Net Changes**: -115 lines (simpler, cleaner code)
- **Broadcast Pattern**: Direct `await broadcast_*()` calls instead of `run_coroutine_threadsafe()`
- **Migration Impact**: Existing tests require async updates (`@pytest.mark.asyncio` and `await` calls)

### Migration Guide for Test Updates
Tests that call worker agent methods need to be updated:
```python
# Before (synchronous)
def test_execute_task(agent):
result = agent.execute_task(task)

# After (asynchronous)
@pytest.mark.asyncio
async def test_execute_task(agent):
result = await agent.execute_task(task)
```

See: `specs/048-async-worker-agents/quickstart.md` for detailed migration instructions.
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# codeframe Development Guidelines

Auto-generated from all feature plans. Last updated: 2025-11-06
Auto-generated from all feature plans. Last updated: 2025-11-07

## Active Technologies
- TypeScript 5.3+ (frontend), Python 3.11+ (backend - existing) (005-project-schema-refactoring)
- Python 3.11 + anthropic (AsyncAnthropic), asyncio, FastAPI, websockets (048-async-worker-agents)

## Project Structure
```
Expand Down
97 changes: 22 additions & 75 deletions codeframe/agents/backend_worker_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,37 +94,6 @@ def __init__(
f"ws_enabled={ws_manager is not None}"
)

def _broadcast_async(
self,
broadcast_func,
*args,
**kwargs
) -> None:
"""
Helper to broadcast WebSocket messages (handles async event loop safely).

Uses asyncio.run_coroutine_threadsafe to schedule coroutines from threads,
avoiding deadlocks when called from thread pool executors.

Args:
broadcast_func: Async function to call (e.g., broadcast_task_status)
*args: Positional arguments for broadcast_func
**kwargs: Keyword arguments for broadcast_func
"""
if not self.ws_manager:
return

try:
loop = asyncio.get_running_loop()
asyncio.run_coroutine_threadsafe(
broadcast_func(*args, **kwargs),
loop
)
except RuntimeError:
logger.debug(
f"Skipped broadcast (no event loop): {broadcast_func.__name__}"
)

def fetch_next_task(self) -> Optional[Dict[str, Any]]:
"""
Fetch highest priority pending task for this project.
Expand Down Expand Up @@ -227,7 +196,7 @@ def build_context(self, task: Dict[str, Any]) -> Dict[str, Any]:
"issue_context": issue_context
}

def generate_code(self, context: Dict[str, Any]) -> Dict[str, Any]:
async def generate_code(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""
Generate code using LLM based on context.

Expand All @@ -250,7 +219,7 @@ def generate_code(self, context: Dict[str, Any]) -> Dict[str, Any]:
"explanation": str # What was changed and why
}
"""
import anthropic
from anthropic import AsyncAnthropic

task = context["task"]
related_symbols = context.get("related_symbols", [])
Expand Down Expand Up @@ -319,11 +288,11 @@ def generate_code(self, context: Dict[str, Any]) -> Dict[str, Any]:
user_prompt = "\n".join(user_prompt_parts)

# Call Anthropic API
client = anthropic.Anthropic(api_key=self.api_key)
client = AsyncAnthropic(api_key=self.api_key)

logger.debug(f"Calling Anthropic API for task {task.get('id', 'unknown')}")

response = client.messages.create(
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=system_prompt,
Expand Down Expand Up @@ -439,22 +408,7 @@ def update_task_status(
if output:
logger.debug(f"Task {task_id} output: {output[:200]}")

# Broadcast status change via WebSocket (cf-45)
if self.ws_manager:
try:
from codeframe.ui.websocket_broadcasts import broadcast_task_status
self._broadcast_async(
broadcast_task_status,
self.ws_manager,
self.project_id,
task_id,
status,
agent_id=agent_id
)
except Exception as e:
logger.debug(f"Failed to broadcast task status: {e}")

def _run_and_record_tests(self, task_id: int) -> None:
async def _run_and_record_tests(self, task_id: int) -> None:
"""
Run tests and record results in database (cf-42 Phase 3).

Expand Down Expand Up @@ -514,8 +468,7 @@ def _run_and_record_tests(self, task_id: int) -> None:
)

# Broadcast test result
self._broadcast_async(
broadcast_test_result,
await broadcast_test_result(
self.ws_manager,
self.project_id,
task_id,
Expand All @@ -533,8 +486,7 @@ def _run_and_record_tests(self, task_id: int) -> None:
else:
activity_message = f"Tests {test_result.status} for task #{task_id} ({test_result.passed}/{test_result.total} passed)"

self._broadcast_async(
broadcast_activity_update,
await broadcast_activity_update(
self.ws_manager,
self.project_id,
"tests_completed",
Expand All @@ -545,7 +497,7 @@ def _run_and_record_tests(self, task_id: int) -> None:
except Exception as e:
logger.debug(f"Failed to broadcast test result: {e}")

def _attempt_self_correction(
async def _attempt_self_correction(
self,
task: Dict[str, Any],
test_result_id: int,
Expand Down Expand Up @@ -621,7 +573,7 @@ def _attempt_self_correction(
context["correction_mode"] = True
context["correction_prompt"] = correction_prompt

generation_result = self.generate_code(context)
generation_result = await self.generate_code(context)

# Extract analysis from generation output
error_analysis = latest_result['output'][:500] if latest_result['output'] else "Test failures detected"
Expand All @@ -641,7 +593,7 @@ def _attempt_self_correction(
"code_changes": []
}

def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: int) -> bool:
async def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: int) -> bool:
"""
Execute self-correction loop to fix failing tests (cf-43).

Expand Down Expand Up @@ -672,8 +624,7 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in
if self.ws_manager:
try:
from codeframe.ui.websocket_broadcasts import broadcast_correction_attempt
self._broadcast_async(
broadcast_correction_attempt,
await broadcast_correction_attempt(
self.ws_manager,
self.project_id,
task_id,
Expand All @@ -685,7 +636,7 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in
logger.debug(f"Failed to broadcast correction attempt: {e}")

# Attempt correction
correction = self._attempt_self_correction(task, initial_test_result_id, attempt_num)
correction = await self._attempt_self_correction(task, initial_test_result_id, attempt_num)

# Record the correction attempt
attempt_id = self.db.create_correction_attempt(
Expand All @@ -708,7 +659,7 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in
continue

# Re-run tests
self._run_and_record_tests(task_id)
await self._run_and_record_tests(task_id)

# Check if tests now pass
test_results = self.db.get_test_results_by_task(task_id)
Expand All @@ -724,17 +675,15 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in
broadcast_correction_attempt,
broadcast_activity_update
)
self._broadcast_async(
broadcast_correction_attempt,
await broadcast_correction_attempt(
self.ws_manager,
self.project_id,
task_id,
attempt_num,
max_attempts,
"success"
)
self._broadcast_async(
broadcast_activity_update,
await broadcast_activity_update(
self.ws_manager,
self.project_id,
"correction_success",
Expand All @@ -757,8 +706,7 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in
try:
from codeframe.ui.websocket_broadcasts import broadcast_correction_attempt
error_summary = f"Status: {latest_result['status'] if latest_result else 'unknown'}"
self._broadcast_async(
broadcast_correction_attempt,
await broadcast_correction_attempt(
self.ws_manager,
self.project_id,
task_id,
Expand Down Expand Up @@ -794,7 +742,7 @@ def _self_correction_loop(self, task: Dict[str, Any], initial_test_result_id: in

return False

def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
async def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute a single task end-to-end.

Expand Down Expand Up @@ -832,13 +780,13 @@ def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
context = self.build_context(task)

# 3. Generate code using LLM
generation_result = self.generate_code(context)
generation_result = await self.generate_code(context)

# 4. Apply file changes
files_modified = self.apply_file_changes(generation_result["files"])

# 5. Run tests (cf-42 Phase 3)
self._run_and_record_tests(task_id)
await self._run_and_record_tests(task_id)

# 6. Check test results and self-correct if needed (cf-43)
test_results = self.db.get_test_results_by_task(task_id)
Expand All @@ -851,7 +799,7 @@ def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
)

# Attempt self-correction (up to 3 attempts)
correction_successful = self._self_correction_loop(task, latest_test["id"])
correction_successful = await self._self_correction_loop(task, latest_test["id"])

if not correction_successful:
# Self-correction failed - mark task as blocked
Expand All @@ -876,8 +824,7 @@ def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
if self.ws_manager:
try:
from codeframe.ui.websocket_broadcasts import broadcast_activity_update
self._broadcast_async(
broadcast_activity_update,
await broadcast_activity_update(
self.ws_manager,
self.project_id,
"task_completed",
Expand Down Expand Up @@ -907,4 +854,4 @@ def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
"files_modified": files_modified,
"output": "",
"error": error
}
}
Loading
Loading