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
54 changes: 13 additions & 41 deletions apps/backend/agents/coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,39 +130,6 @@
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 @@ -1326,14 +1293,19 @@ def _validate_and_fix_implementation_plan() -> tuple[bool, list[str]]:
# 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,
)
try:
from plugins.runtime import augment_direct_api_prompt

prompt = augment_direct_api_prompt(
prompt,
provider.name,
project_dir,
spec_dir,
agent_type_for_session,
runtime_metadata,
)
except Exception as exc:
logger.warning("Failed to apply plugin prompt augmentations: %s", exc)

runtime_phase = (
"planning" if current_log_phase == LogPhase.PLANNING else "coding"
Expand Down
18 changes: 18 additions & 0 deletions apps/backend/agents/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,24 @@ async def run_followup_planner(
# Generate follow-up planner prompt
prompt = get_followup_planner_prompt(spec_dir)

# 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).
try:
from plugins.runtime import augment_direct_api_prompt

prompt = augment_direct_api_prompt(
prompt,
provider_name,
project_dir,
spec_dir,
"planner",
None,
)
except Exception as exc:
logger.warning("Failed to apply plugin prompt augmentations: %s", exc)

# Run prevention scanner before planning
print_status("Running prevention scanner...", "progress")
try:
Expand Down
31 changes: 31 additions & 0 deletions apps/backend/plugins/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,37 @@ def apply_plugin_prompt_augmentations(
return append_prompt_augmentations(base_prompt, contributions)


def augment_direct_api_prompt(
prompt: str,
provider_name: str,
project_dir: Path,
spec_dir: Path,
agent_type: str,
metadata: dict[str, Any] | None = None,
) -> str:
"""Append plugin 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 run.
"""
if provider_name == "claude":
return prompt
try:
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


async def _maybe_await(value: Any) -> Any:
if inspect.isawaitable(value):
return await value
Expand Down
10 changes: 6 additions & 4 deletions docs/guides/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ write hooks defensively:
| `before_session` / `after_session` / `on_message` | ✅ | ✅ | ✅ |
| `pre_tool` (block) | ✅ | ✅ | ❌ (opaque CLI loop) |
| `post_tool` (observe) | ✅ | ✅ (observational) | ❌ |
| `augment_prompt` | ✅ | see note | ❌ |
| `augment_prompt` | ✅ | ✅ | ✅ (via message) |

Codex and other CLI backends run their own tool loop inside a separate process,
so per-tool interception is not possible there — only prompt-level and lifecycle
Expand Down Expand Up @@ -255,9 +255,11 @@ use a stable extension name: `Delete`, `Move`, `ApplyPatch`. The mapping lives i
unless read-only hooking is explicitly enabled, so plugins don't run on every
file read.

> **Note (`augment_prompt`)**: prompt augmentation currently reaches the Claude
> SDK backend only. Cross-backend prompt augmentation is tracked as separate
> work; do not rely on `augment_prompt` for Direct-API/Codex runs yet.
> **Note (`augment_prompt`)**: the Claude SDK path applies augmentation to the
> system prompt inside `create_client()`; Direct-API/Codex backends carry their
> instructions in the agent message, so the same plugin contributions are
> appended there instead (guarded on provider name so the Claude path is never
> augmented twice). Applied via `augment_direct_api_prompt` in `plugins.runtime`.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""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.
Direct providers (OpenAI, Codex, ...) carry their instructions in the agent
message, so ``augment_direct_api_prompt`` appends the same plugin contributions
there — but only for non-claude providers, so the claude path is never augmented
twice. Both the coder loop and the follow-up planner call this shared helper.
"""

from __future__ import annotations
Expand All @@ -15,7 +16,7 @@
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "apps" / "backend"))

from agents.coder import _augment_direct_api_prompt
from plugins.runtime import augment_direct_api_prompt

_TARGET = "plugins.runtime.apply_plugin_prompt_augmentations"

Expand All @@ -24,7 +25,7 @@ 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(
out = augment_direct_api_prompt(
"BASE", "claude", tmp_path, tmp_path, "coder", None
)
assert out == "BASE"
Expand All @@ -34,7 +35,7 @@ def test_claude_provider_is_not_augmented(tmp_path):
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(
out = augment_direct_api_prompt(
"BASE", "openai", tmp_path, tmp_path, "coder", {"subtask_id": "s1"}
)
apply_mock.assert_called_once_with(
Expand All @@ -45,7 +46,7 @@ def test_direct_provider_is_augmented(tmp_path):

def test_codex_provider_is_augmented(tmp_path):
with patch(_TARGET, return_value="BASE+X") as apply_mock:
out = _augment_direct_api_prompt(
out = augment_direct_api_prompt(
"BASE", "codex", tmp_path, tmp_path, "planner", None
)
apply_mock.assert_called_once()
Expand All @@ -54,7 +55,7 @@ def test_codex_provider_is_augmented(tmp_path):

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