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
47 changes: 47 additions & 0 deletions apps/backend/agents/coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,39 @@
logger = logging.getLogger(__name__)


def _augment_direct_api_prompt(
prompt: str,
provider_name: str,
project_dir: Path,
spec_dir: Path,
agent_type: str,
metadata: dict[str, object] | None,
) -> str:
"""Append enabled plugins' prompt augmentations to a Direct-API agent message.

The Claude SDK path augments the system prompt inside ``create_client()``,
so this is a no-op for the ``claude`` provider to avoid double-applying.
Direct providers (OpenAI, Codex, ...) carry their instructions in the
message, so the same plugin contributions are appended there. Any failure
degrades to the original prompt rather than breaking the build.
"""
if provider_name == "claude":
return prompt
try:
from plugins.runtime import apply_plugin_prompt_augmentations

return apply_plugin_prompt_augmentations(
prompt,
project_dir,
spec_dir,
agent_type,
metadata=metadata,
)
except Exception as exc:
logger.warning("Failed to apply plugin prompt augmentations: %s", exc)
return prompt


# =============================================================================
# FILE VALIDATION UTILITIES
# =============================================================================
Expand Down Expand Up @@ -1288,6 +1321,20 @@ def _validate_and_fix_implementation_plan() -> tuple[bool, list[str]]:
analysis_only_terminal_message: str | None = None

provider = create_engine_provider(provider_config)

# Plugin prompt augmentation for Direct-API / Codex backends. The Claude
# SDK path augments the system prompt inside create_client(); direct
# providers carry their instructions in the message, so append the same
# plugin contributions there (no-op for claude to avoid double-applying).
prompt = _augment_direct_api_prompt(
prompt,
provider.name,
project_dir,
spec_dir,
agent_type_for_session,
runtime_metadata,
)

runtime_phase = (
"planning" if current_log_phase == LogPhase.PLANNING else "coding"
)
Expand Down
60 changes: 60 additions & 0 deletions tests/test_coder_prompt_augmentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Tests for cross-backend plugin prompt augmentation on the Direct-API path.

The Claude SDK backend augments the system prompt inside ``create_client()``.
Direct providers carry their instructions in the agent message, so coder.py
appends the same plugin contributions to that message — but only for non-claude
providers, so the claude path is never augmented twice.
"""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import patch

REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "apps" / "backend"))

from agents.coder import _augment_direct_api_prompt

_TARGET = "plugins.runtime.apply_plugin_prompt_augmentations"


def test_claude_provider_is_not_augmented(tmp_path):
# Claude already augments via create_client(); the message must be left
# untouched so contributions are not applied twice.
with patch(_TARGET) as apply_mock:
out = _augment_direct_api_prompt(
"BASE", "claude", tmp_path, tmp_path, "coder", None
)
assert out == "BASE"
apply_mock.assert_not_called()


def test_direct_provider_is_augmented(tmp_path):
augmented = "BASE\n\n# Plugin Runtime Instructions\n## demo\nhello"
with patch(_TARGET, return_value=augmented) as apply_mock:
out = _augment_direct_api_prompt(
"BASE", "openai", tmp_path, tmp_path, "coder", {"subtask_id": "s1"}
)
apply_mock.assert_called_once_with(
"BASE", tmp_path, tmp_path, "coder", metadata={"subtask_id": "s1"}
)
assert out == augmented


def test_codex_provider_is_augmented(tmp_path):
with patch(_TARGET, return_value="BASE+X") as apply_mock:
out = _augment_direct_api_prompt(
"BASE", "codex", tmp_path, tmp_path, "planner", None
)
apply_mock.assert_called_once()
assert out == "BASE+X"


def test_augmentation_failure_degrades_to_base_prompt(tmp_path):
with patch(_TARGET, side_effect=RuntimeError("boom")):
out = _augment_direct_api_prompt(
"BASE", "openai", tmp_path, tmp_path, "coder", None
)
assert out == "BASE"
Loading