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
100 changes: 100 additions & 0 deletions codeframe/core/agent_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Agent adapter protocol for CodeFRAME.

Defines the interface that any coding agent (Claude Code, Codex, Aider, built-in)
must implement to be used as a CodeFrame execution engine.

This module is headless - no FastAPI or HTTP dependencies.
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Iterator, Protocol, runtime_checkable


class AgentResultStatus(str, Enum):
"""Terminal status from an agent execution."""

COMPLETED = "completed"
FAILED = "failed"
BLOCKED = "blocked"
TIMEOUT = "timeout"


@dataclass
class AdapterTokenUsage:
"""Lightweight token usage for adapter results."""

input_tokens: int
output_tokens: int
model: str | None = None
cost_usd: float | None = None

@property
def total_tokens(self) -> int:
return self.input_tokens + self.output_tokens


@dataclass
class AgentContext:
"""Everything CodeFrame provides to an execution engine."""

task_id: str
task_title: str
task_description: str
prd_content: str | None = None
tech_stack: str | None = None
project_preferences: str | None = None
relevant_files: list[str] = field(default_factory=list)
file_contents: dict[str, str] = field(default_factory=dict)
blocker_history: list[str] = field(default_factory=list)
dependency_context: str | None = None
verification_gates: list[str] = field(default_factory=list)
attempt: int = 0
previous_errors: list[str] = field(default_factory=list)


@dataclass
class AgentResult:
"""What every execution engine returns to CodeFrame."""

status: AgentResultStatus
summary: str
files_modified: list[str] = field(default_factory=list)
files_created: list[str] = field(default_factory=list)
error: str | None = None
blocker_question: str | None = None
token_usage: AdapterTokenUsage | None = None
duration_ms: int = 0


@dataclass
class AgentEvent:
"""Progress event yielded during agent execution."""

type: str
message: str
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
metadata: dict = field(default_factory=dict)


@runtime_checkable
class AgentAdapter(Protocol):
"""Interface for any coding agent that CodeFrame can orchestrate."""

def execute(
self,
task_prompt: str,
workspace_path: Path,
context: AgentContext,
timeout_ms: int = 3_600_000,
) -> AgentResult: ...

def stream_events(self) -> Iterator[AgentEvent]: ...
Comment on lines +82 to +94

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from typing import Protocol, runtime_checkable

`@runtime_checkable`
class BaseAdapter(Protocol):
    def execute(self) -> int: ...
    def stream_events(self): ...

class NoStream:
    def execute(self) -> int:
        return 1

class WithStream:
    def execute(self) -> int:
        return 1
    def stream_events(self):
        return iter(())

print("NoStream:", isinstance(NoStream(), BaseAdapter))
print("WithStream:", isinstance(WithStream(), BaseAdapter))
PY

Repository: frankbria/codeframe

Length of output: 95


🏁 Script executed:

# First, locate and read the agent_adapter.py file to verify the current protocol
fd agent_adapter.py --type f

Repository: frankbria/codeframe

Length of output: 210


🏁 Script executed:

# Search for AgentAdapter implementations to see if any omit stream_events
rg "class.*AgentAdapter" -A 10 --type py

Repository: frankbria/codeframe

Length of output: 3286


🏁 Script executed:

# Check how stream_events is used across the codebase
rg "stream_events" --type py -B 2 -A 2

Repository: frankbria/codeframe

Length of output: 2599


🏁 Script executed:

# Read the actual agent_adapter.py file to see the complete protocol definition
cat -n codeframe/core/agent_adapter.py | head -100

Repository: frankbria/codeframe

Length of output: 3469


🏁 Script executed:

# Check the test that validates protocol compliance without stream_events
cat -n tests/core/test_agent_adapter.py | grep -A 15 "PartialAdapter"

Repository: frankbria/codeframe

Length of output: 1144


🏁 Script executed:

# Look at how AgentAdapter is actually used in the codebase
rg "isinstance.*AgentAdapter" --type py -B 2 -A 2

Repository: frankbria/codeframe

Length of output: 6108


🏁 Script executed:

# Check if any existing adapter implementations skip stream_events
rg "class.*Adapter" codeframe/core/adapters --type py -A 30 | grep -E "(class|def stream_events)" | head -40

Repository: frankbria/codeframe

Length of output: 816


🏁 Script executed:

# Look at how stream_events is actually called in the codebase
rg "\.stream_events\(\)" --type py -B 3 -A 3

Repository: frankbria/codeframe

Length of output: 546


🏁 Script executed:

# Check the adapters subdirectory structure
ls -la codeframe/core/adapters/

Repository: frankbria/codeframe

Length of output: 630


🏁 Script executed:

# Check if there's any design documentation or objectives about streaming being optional
fd -e md -e txt | xargs rg -l "streaming.*optional|optional.*stream" 2>/dev/null

Repository: frankbria/codeframe

Length of output: 72


🏁 Script executed:

# Check if there are any comments in the adapters about streaming being optional
rg "optional|streaming" codeframe/core/adapters --type py -B 2 -A 2 | head -50

Repository: frankbria/codeframe

Length of output: 1364


🏁 Script executed:

# Read the README to check for design objectives about optional streaming
cat README.md | head -100

Repository: frankbria/codeframe

Length of output: 3939


🏁 Script executed:

# Also check if there's a CLAUDE.md or design doc that mentions streaming
fd -e md | xargs grep -l "objective\|design\|streaming" 2>/dev/null | head -10

Repository: frankbria/codeframe

Length of output: 385


🏁 Script executed:

# Check the actual implementation of stream_events across existing adapters
for file in codeframe/core/adapters/*.py; do echo "=== $file ==="; grep -A 5 "def stream_events" "$file" 2>/dev/null || echo "No stream_events found"; done

Repository: frankbria/codeframe

Length of output: 560


🏁 Script executed:

# Search for stream_events implementations more broadly
rg "def stream_events" --type py -B 3 -A 5

Repository: frankbria/codeframe

Length of output: 1106


🏁 Script executed:

# Check the builtin adapter more carefully for stream_events
cat -n codeframe/core/adapters/builtin.py | head -150

Repository: frankbria/codeframe

Length of output: 6191


🏁 Script executed:

# Check the subprocess adapter which is the base class
cat -n codeframe/core/adapters/subprocess_adapter.py | head -100

Repository: frankbria/codeframe

Length of output: 4019


🏁 Script executed:

# Check if BuiltinReactAdapter actually implements stream_events anywhere
cat -n codeframe/core/adapters/builtin.py | tail -100

Repository: frankbria/codeframe

Length of output: 4288


🏁 Script executed:

# Check if these adapters implement requires_api_key property
rg "requires_api_key" --type py -B 2 -A 3

Repository: frankbria/codeframe

Length of output: 3102


🏁 Script executed:

# Look at a test that checks isinstance for these actual adapters
cat -n tests/core/adapters/test_builtin.py | head -80

Repository: frankbria/codeframe

Length of output: 3549


🏁 Script executed:

# Check if BuiltinReactAdapter implements execute method (not just run)
rg "def execute" codeframe/core/adapters/builtin.py

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Check if there are any methods we're missing in the builtin adapters
cat -n codeframe/core/adapters/builtin.py | grep -E "def |@property"

Repository: frankbria/codeframe

Length of output: 520


🏁 Script executed:

# Run the actual test to see if it passes
cd codeframe && python -m pytest tests/core/adapters/test_builtin.py::TestBuiltinReactAdapter::test_conforms_to_protocol -v 2>&1 || echo "Test failed or pytest not available"

Repository: frankbria/codeframe

Length of output: 1035


🏁 Script executed:

# Find the actual test file location
find . -name "*test_builtin.py" -type f

Repository: frankbria/codeframe

Length of output: 100


🏁 Script executed:

# Check if there's an execute method defined somewhere else or if run is being aliased
rg "def execute|execute.*=" codeframe/core/adapters/ --type py

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Look for where AgentAdapter methods are actually being used in the tests
cat -n tests/core/adapters/test_builtin.py | grep -A 5 "conforms_to_protocol"

Repository: frankbria/codeframe

Length of output: 705


🏁 Script executed:

# Check if BuiltinReactAdapter has stream_events and requires_api_key somewhere
grep -n "stream_events\|requires_api_key\|execute" codeframe/core/adapters/builtin.py

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Run the actual test to see what happens
python -m pytest ./tests/core/adapters/test_builtin.py::TestBuiltinReactAdapter::test_conforms_to_protocol -xvs 2>&1 | head -50

Repository: frankbria/codeframe

Length of output: 2625


🏁 Script executed:

# Check if maybe there's a base class or mixin being used
head -20 codeframe/core/adapters/builtin.py

Repository: frankbria/codeframe

Length of output: 761


🏁 Script executed:

# Check if BuiltinReactAdapter is missing execute, stream_events, requires_api_key
python3 << 'EOF'
from typing import Protocol, runtime_checkable, Iterator

`@runtime_checkable`
class AgentAdapter(Protocol):
    def execute(self): ...
    def stream_events(self) -> Iterator: ...
    `@property`
    def name(self) -> str: ...
    `@property`
    def requires_api_key(self) -> dict: ...

class BuiltinReactAdapterMock:
    `@property`
    def name(self) -> str:
        return "react"
    def run(self): 
        pass
    # Missing: execute, stream_events, requires_api_key

adapter = BuiltinReactAdapterMock()
print("BuiltinReactAdapter conforms to AgentAdapter:", isinstance(adapter, AgentAdapter))
print("\nMissing members:")
print("- execute:", not hasattr(adapter, 'execute'))
print("- stream_events:", not hasattr(adapter, 'stream_events'))
print("- requires_api_key:", not hasattr(adapter, 'requires_api_key'))
EOF

Repository: frankbria/codeframe

Length of output: 195


🏁 Script executed:

# Let's check what the actual protocol definition looks like and what adapters implement
python3 << 'EOF'
import sys
sys.path.insert(0, '/home/jailuser/git')

# Check what methods BuiltinReactAdapter actually has
from codeframe.core.adapters.builtin import BuiltinReactAdapter

print("BuiltinReactAdapter methods and properties:")
for attr in dir(BuiltinReactAdapter):
    if not attr.startswith('_'):
        print(f"  {attr}")

# Check specifically for protocol required members
required = ['execute', 'stream_events', 'name', 'requires_api_key']
actual = set(dir(BuiltinReactAdapter))
for member in required:
    status = "✓" if member in actual else "✗"
    print(f"{status} {member}")
EOF

Repository: frankbria/codeframe

Length of output: 801


🏁 Script executed:

# Check if there's any dynamic method assignment or aliasing happening
rg "execute.*=|setattr.*execute" codeframe/core/adapters/ --type py

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Check if the adapters actually define execute or if they're supposed to alias run
cat -n codeframe/core/adapters/subprocess_adapter.py | grep -A 20 "def run"

Repository: frankbria/codeframe

Length of output: 901


🏁 Script executed:

# Let's check what the protocol actually expects vs what's being used
rg "\.execute\(" codeframe/core --type py -B 2 -A 2 | head -40

Repository: frankbria/codeframe

Length of output: 1785


🏁 Script executed:

# Look at the complete protocol definition to see if there's something with aliases or __getattr__
cat -n codeframe/core/agent_adapter.py

Repository: frankbria/codeframe

Length of output: 3469


🏁 Script executed:

# Check if there are two different AgentAdapter definitions or if one is importing from another
find codeframe -name "*.py" -type f -exec grep -l "class AgentAdapter" {} \;

Repository: frankbria/codeframe

Length of output: 135


🏁 Script executed:

# Look at what the comment about the "linked objective" might refer to - check for design docs
cat CLAUDE.md 2>/dev/null | grep -i "stream\|optional" | head -20

Repository: frankbria/codeframe

Length of output: 1293


The AgentAdapter protocol requires methods that no adapter implementations actually provide.

The protocol at lines 82–100 mandates execute(), stream_events(), name, and requires_api_key. However, all actual adapters (BuiltinReactAdapter, BuiltinPlanAdapter, SubprocessAdapter) implement run() instead of execute(), and are missing stream_events() and requires_api_key entirely. This means isinstance(adapter, AgentAdapter) would return False for every adapter in the codebase, despite test claims of protocol conformance. The protocol definition needs to match actual implementations—either rename execute() to run(), make stream_events() optional by splitting it into a separate protocol, or update adapters to provide the required surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/agent_adapter.py` around lines 82 - 94, The AgentAdapter
protocol doesn't match implementations: change the protocol to declare run(...)
instead of execute(...), remove stream_events() and requires_api_key from the
core AgentAdapter, and create two small optional protocols—StreamableAgent (with
stream_events() -> Iterator[AgentEvent]) and ApiKeyAgent (with requires_api_key:
bool or def requires_api_key() -> bool)—so BuiltinReactAdapter,
BuiltinPlanAdapter, and SubprocessAdapter will be recognized as implementing
AgentAdapter (and can opt into StreamableAgent/ApiKeyAgent if they support those
features); update any type checks or isinstance usages to reference the new
optional protocols where streaming or API-key behavior is required.


@property
def name(self) -> str: ...

@property
def requires_api_key(self) -> dict[str, str]: ...
284 changes: 284 additions & 0 deletions tests/core/test_agent_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
"""Tests for AgentAdapter protocol and supporting types.

Validates:
- Dataclass construction with defaults and full params
- AgentResultStatus enum values
- AgentAdapter protocol compliance via @runtime_checkable
- Streaming iterator contract
"""

import pytest
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterator

Comment thread
coderabbitai[bot] marked this conversation as resolved.
pytestmark = pytest.mark.v2


class TestAgentResultStatus:
"""AgentResultStatus enum covers all terminal states."""

def test_has_completed(self):
from codeframe.core.agent_adapter import AgentResultStatus
assert AgentResultStatus.COMPLETED.value == "completed"

def test_has_failed(self):
from codeframe.core.agent_adapter import AgentResultStatus
assert AgentResultStatus.FAILED.value == "failed"

def test_has_blocked(self):
from codeframe.core.agent_adapter import AgentResultStatus
assert AgentResultStatus.BLOCKED.value == "blocked"

def test_has_timeout(self):
from codeframe.core.agent_adapter import AgentResultStatus
assert AgentResultStatus.TIMEOUT.value == "timeout"

def test_is_str_enum(self):
from codeframe.core.agent_adapter import AgentResultStatus
assert isinstance(AgentResultStatus.COMPLETED, str)


class TestAdapterTokenUsage:
"""Lightweight token usage dataclass."""

def test_minimal_construction(self):
from codeframe.core.agent_adapter import AdapterTokenUsage
usage = AdapterTokenUsage(input_tokens=100, output_tokens=50)
assert usage.input_tokens == 100
assert usage.output_tokens == 50
assert usage.model is None
assert usage.cost_usd is None

def test_full_construction(self):
from codeframe.core.agent_adapter import AdapterTokenUsage
usage = AdapterTokenUsage(
input_tokens=1000,
output_tokens=500,
model="claude-sonnet-4-20250514",
cost_usd=0.015,
)
assert usage.model == "claude-sonnet-4-20250514"
assert usage.cost_usd == 0.015

def test_total_tokens(self):
from codeframe.core.agent_adapter import AdapterTokenUsage
usage = AdapterTokenUsage(input_tokens=100, output_tokens=50)
assert usage.total_tokens == 150


class TestAgentContext:
"""AgentContext captures all context CodeFrame provides to engines."""

def test_minimal_construction(self):
from codeframe.core.agent_adapter import AgentContext
ctx = AgentContext(
task_id="task-1",
task_title="Implement feature X",
task_description="Add X to the system",
)
assert ctx.task_id == "task-1"
assert ctx.prd_content is None
assert ctx.tech_stack is None
assert ctx.project_preferences is None
assert ctx.relevant_files == []
assert ctx.file_contents == {}
assert ctx.blocker_history == []
assert ctx.dependency_context is None
assert ctx.verification_gates == []
assert ctx.attempt == 0
assert ctx.previous_errors == []

def test_full_construction(self):
from codeframe.core.agent_adapter import AgentContext
ctx = AgentContext(
task_id="task-42",
task_title="Fix auth bug",
task_description="Session tokens expire too early",
prd_content="# Auth PRD\nTokens should last 24h",
tech_stack="Python with FastAPI",
project_preferences="Use ruff for linting",
relevant_files=["auth.py", "tests/test_auth.py"],
file_contents={"auth.py": "def login(): pass"},
blocker_history=["Previous: needed DB access"],
dependency_context="Task-41 created the auth module",
verification_gates=["ruff", "pytest"],
attempt=2,
previous_errors=["ImportError: no module named jwt"],
)
assert ctx.task_id == "task-42"
assert len(ctx.relevant_files) == 2
assert ctx.attempt == 2
assert len(ctx.previous_errors) == 1

def test_list_defaults_are_independent(self):
"""Ensure default_factory creates independent lists (no shared mutable state)."""
from codeframe.core.agent_adapter import AgentContext
ctx1 = AgentContext(task_id="1", task_title="A", task_description="A")
ctx2 = AgentContext(task_id="2", task_title="B", task_description="B")
ctx1.relevant_files.append("file.py")
assert ctx2.relevant_files == []


class TestAgentResult:
"""AgentResult captures outcome from any engine."""

def test_minimal_construction(self):
from codeframe.core.agent_adapter import AgentResult, AgentResultStatus
result = AgentResult(
status=AgentResultStatus.COMPLETED,
summary="Added feature X",
)
assert result.status == AgentResultStatus.COMPLETED
assert result.files_modified == []
assert result.files_created == []
assert result.error is None
assert result.blocker_question is None
assert result.token_usage is None
assert result.duration_ms == 0

def test_failed_result(self):
from codeframe.core.agent_adapter import AgentResult, AgentResultStatus
result = AgentResult(
status=AgentResultStatus.FAILED,
summary="Could not implement",
error="ImportError: missing dependency",
duration_ms=5000,
)
assert result.status == AgentResultStatus.FAILED
assert result.error is not None

def test_blocked_result_with_question(self):
from codeframe.core.agent_adapter import AgentResult, AgentResultStatus
result = AgentResult(
status=AgentResultStatus.BLOCKED,
summary="Need clarification on auth approach",
blocker_question="Should we use JWT or session cookies?",
)
assert result.blocker_question is not None

def test_result_with_token_usage(self):
from codeframe.core.agent_adapter import (
AdapterTokenUsage, AgentResult, AgentResultStatus,
)
result = AgentResult(
status=AgentResultStatus.COMPLETED,
summary="Done",
token_usage=AdapterTokenUsage(input_tokens=1000, output_tokens=500),
files_modified=["auth.py"],
files_created=["tests/test_auth.py"],
duration_ms=12000,
)
assert result.token_usage.total_tokens == 1500
assert result.files_modified == ["auth.py"]
assert result.duration_ms == 12000


class TestAgentEvent:
"""AgentEvent supports progress streaming."""

def test_minimal_construction(self):
from codeframe.core.agent_adapter import AgentEvent
event = AgentEvent(type="progress", message="Working on step 1")
assert event.type == "progress"
assert event.message == "Working on step 1"
assert isinstance(event.timestamp, datetime)
assert event.metadata == {}

def test_with_metadata(self):
from codeframe.core.agent_adapter import AgentEvent
ts = datetime(2026, 3, 9, tzinfo=timezone.utc)
event = AgentEvent(
type="file_changed",
message="Modified auth.py",
timestamp=ts,
metadata={"file": "auth.py", "lines_changed": 15},
)
assert event.timestamp == ts
assert event.metadata["lines_changed"] == 15

def test_event_types_are_strings(self):
from codeframe.core.agent_adapter import AgentEvent
for event_type in ("progress", "file_changed", "command_run", "error"):
event = AgentEvent(type=event_type, message="test")
assert event.type == event_type


class TestAgentAdapterProtocol:
"""AgentAdapter protocol compliance via @runtime_checkable."""

def _make_compliant_class(self):
"""Create a minimal class that satisfies AgentAdapter."""
from codeframe.core.agent_adapter import (
AgentContext, AgentEvent, AgentResult, AgentResultStatus,
)

class FakeAdapter:
def execute(
self,
task_prompt: str,
workspace_path: Path,
context: AgentContext,
timeout_ms: int = 3_600_000,
) -> AgentResult:
return AgentResult(
status=AgentResultStatus.COMPLETED,
summary="fake",
duration_ms=100,
)

def stream_events(self) -> Iterator[AgentEvent]:
yield AgentEvent(type="progress", message="working")

@property
def name(self) -> str:
return "fake"

@property
def requires_api_key(self) -> dict[str, str]:
return {}

return FakeAdapter

def test_compliant_class_satisfies_protocol(self):
from codeframe.core.agent_adapter import AgentAdapter
FakeAdapter = self._make_compliant_class()
adapter = FakeAdapter()
assert isinstance(adapter, AgentAdapter)

def test_non_compliant_class_fails(self):
from codeframe.core.agent_adapter import AgentAdapter

class NotAnAdapter:
pass

assert not isinstance(NotAnAdapter(), AgentAdapter)

def test_partial_implementation_fails(self):
from codeframe.core.agent_adapter import AgentAdapter

class PartialAdapter:
def execute(self, task_prompt, workspace_path, context, timeout_ms=0):
pass
# Missing: stream_events, name, requires_api_key

assert not isinstance(PartialAdapter(), AgentAdapter)
Comment on lines +257 to +265

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 | 🟠 Major

This test hard-codes stream_events as mandatory.

Issue #409 describes streaming support as optional, but this negative case rejects adapters that omit stream_events. That bakes the wrong contract into the suite and will fail valid non-streaming adapters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_agent_adapter.py` around lines 257 - 265, The test currently
treats stream_events as a required attribute but streaming is optional; modify
test_partial_implementation_fails so it verifies failure only when truly
required members are missing: keep execute but remove stream_events from the
“missing” list and instead omit a genuinely required attribute such as name or
requires_api_key (or both) on PartialAdapter, and assert that an instance of
that incomplete PartialAdapter is not an AgentAdapter; reference
test_partial_implementation_fails, AgentAdapter, PartialAdapter, stream_events,
name, and requires_api_key when making the change.


def test_execute_returns_agent_result(self):
from codeframe.core.agent_adapter import (
AgentContext, AgentResult, AgentResultStatus,
)
FakeAdapter = self._make_compliant_class()
adapter = FakeAdapter()
ctx = AgentContext(task_id="1", task_title="Test", task_description="Test")
result = adapter.execute("do something", Path("/tmp"), ctx)
assert isinstance(result, AgentResult)
assert result.status == AgentResultStatus.COMPLETED

def test_stream_events_yields_agent_events(self):
from codeframe.core.agent_adapter import AgentEvent
FakeAdapter = self._make_compliant_class()
adapter = FakeAdapter()
events_list = list(adapter.stream_events())
assert len(events_list) == 1
assert isinstance(events_list[0], AgentEvent)
Comment on lines +243 to +284

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.

🛠️ Refactor suggestion | 🟠 Major

Add at least one compatibility test against the real ReactAgent adaptation path.

These assertions only prove the protocol against FakeAdapter. The acceptance criteria for this PR explicitly call out ensuring the existing ReactAgent can be adapted, and that can still regress silently with the current suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_agent_adapter.py` around lines 243 - 284, Add a real
compatibility test that exercises the existing ReactAgent adaptation path:
import ReactAgent (the concrete agent implementation), instantiate it, and
assert it satisfies the AgentAdapter protocol just like FakeAdapter does—i.e.,
assert isinstance(ReactAgent(), AgentAdapter) and also call execute(...) and
stream_events() on the ReactAgent instance and assert the returned types are
AgentResult and AgentEvent respectively (matching the existing tests
test_compliant_class_satisfies_protocol, test_execute_returns_agent_result, and
test_stream_events_yields_agent_events).

Loading