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
40 changes: 15 additions & 25 deletions libs/code/deepagents_code/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2474,21 +2474,17 @@ def _subagent_cli_middleware(
# Server-owned hooks must wrap subagent tools too; otherwise Pre/Post
# ToolUse only fire on the parent graph. Disable Stop so finishing a
# subagent does not emit the main-agent Stop event (SubagentStop still
# fires from the parent wrap around `task`). Hooks v2 stays off unless
# experimental mode is on.
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy

if is_env_truthy(EXPERIMENTAL):
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware
# fires from the parent wrap around `task`).
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware

hooks_cwd = Path(effective_cwd) if effective_cwd is not None else Path.cwd()
middleware.append(
ServerHooksMiddleware(
cwd=hooks_cwd,
emit_stop=False,
mcp_tools=mcp_tools,
)
hooks_cwd = Path(effective_cwd) if effective_cwd is not None else Path.cwd()
middleware.append(
ServerHooksMiddleware(
cwd=hooks_cwd,
emit_stop=False,
mcp_tools=mcp_tools,
)
)
# Subagents share the on-disk filesystem backend and can edit the user
# AGENTS.md, so they get the same managed onboarding-name block guard as
# the main agent. Gated on memory because the block only exists when
Expand Down Expand Up @@ -2883,19 +2879,13 @@ def _subagent_cli_middleware(
agent_middleware.append(AsyncApprovalHITLMiddleware(resolved_interrupt_on))

# Server-owned Hooks v2 lifecycle events (Pre/Post tool, Stop, subagent).
# Mounted only in experimental mode; when mounted, also gated at runtime by
# `hooks_server_events` on the per-run context so idle sessions without
# configured handlers pay no interrupt round-trip. Appended after the HITL
# middleware so `PreToolUse` resolves before approval routing.
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
# Gated at runtime by `hooks_server_events` on the per-run context so idle
# sessions without configured handlers pay no interrupt round-trip. Appended
# after the HITL middleware so `PreToolUse` resolves before approval routing.
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware

if is_env_truthy(EXPERIMENTAL):
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware

hooks_cwd = Path(effective_cwd) if effective_cwd is not None else Path.cwd()
agent_middleware.append(
ServerHooksMiddleware(cwd=hooks_cwd, mcp_tools=mcp_tools)
)
hooks_cwd = Path(effective_cwd) if effective_cwd is not None else Path.cwd()
agent_middleware.append(ServerHooksMiddleware(cwd=hooks_cwd, mcp_tools=mcp_tools))

if fs_tools is not None:
# `fs_tools` is an explicit allowlist here (`--allow-fs-tools all` and an
Expand Down
5 changes: 0 additions & 5 deletions libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4746,11 +4746,6 @@ async def _retarget_hooks_after_cwd_switch(
immediately. In-session resumes defer activation until the outgoing
runtime has received `SessionEnd`.
"""
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy

if not is_env_truthy(EXPERIMENTAL):
return

from deepagents_code.hooks.loading import project_hooks_path
from deepagents_code.hooks.trust import project_root_for, trust_project_hooks
from deepagents_code.tui.widgets.cwd_switch import HookTrustScreen
Expand Down
10 changes: 1 addition & 9 deletions libs/code/deepagents_code/hooks/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,6 @@ def create(
) -> HooksManager:
"""Load hook configuration and return a ready manager.

Hooks v2 is in progress, so it stays off unless
`DEEPAGENTS_CODE_EXPERIMENTAL` is truthy; without it the manager is
inert and no hook (client- or server-owned) fires.

Never raises: a failed load yields an inert manager whose lifecycle
methods are all no-ops.

Expand Down Expand Up @@ -614,16 +610,12 @@ def _load_runtime(
plugins: Already-discovered plugins, or `None` to discover them.

Returns:
The loaded runtime, `None` when configuration could not be loaded, and
`None` whenever `DEEPAGENTS_CODE_EXPERIMENTAL` is not truthy.
The loaded runtime, or `None` when configuration could not be loaded.
"""
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
from deepagents_code.hooks.runtime import HooksRuntime
from deepagents_code.plugins.adapters.hooks import discover_plugin_hook_sources
from deepagents_code.project_utils import ProjectContext

if not is_env_truthy(EXPERIMENTAL):
return None
try:
project_context = ProjectContext.from_user_cwd(cwd)
plugin_sources, plugin_diagnostics = discover_plugin_hook_sources(
Expand Down
7 changes: 1 addition & 6 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3714,14 +3714,11 @@ def _check_project_hooks_trust(
Returns:
The trust policy to run the session under, `INTERRUPTED` when the user
presses Ctrl+C, or `CANCELLED` when the user presses Esc or Ctrl+D to
abort startup. Nothing is trusted, and no prompt is shown, unless
`DEEPAGENTS_CODE_EXPERIMENTAL` is truthy, since hooks stay off without
it.
abort startup.
"""
from rich.console import Console
from rich.text import Text

from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
from deepagents_code.hooks.loading import project_hooks_path
from deepagents_code.hooks.trust import (
WorkspaceTrust,
Expand All @@ -3730,8 +3727,6 @@ def _check_project_hooks_trust(
)
from deepagents_code.project_utils import ProjectContext

if not is_env_truthy(EXPERIMENTAL):
return WorkspaceTrust.none()
try:
context = ProjectContext.from_user_cwd(Path.cwd())
project_root = context.project_root or context.user_cwd
Expand Down
11 changes: 2 additions & 9 deletions libs/code/tests/unit_tests/hooks/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,16 @@
import json
from typing import TYPE_CHECKING

import pytest

from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.approval_mode import ApprovalMode
from deepagents_code.hooks.manager import HookSessionIdentity, HooksManager
from deepagents_code.hooks.models.domain import PermissionEffect

if TYPE_CHECKING:
from pathlib import Path

from deepagents_code.hooks.presenter import HookNoticeSeverity, HookPresenter

import pytest

@pytest.fixture(autouse=True)
def _enable_hooks_v2(monkeypatch: pytest.MonkeyPatch) -> None:
"""Hooks v2 only loads in experimental mode, which these tests exercise."""
monkeypatch.setenv(EXPERIMENTAL, "1")
from deepagents_code.hooks.presenter import HookNoticeSeverity, HookPresenter


def _write_project_hooks(root: Path) -> Path:
Expand Down
7 changes: 1 addition & 6 deletions libs/code/tests/unit_tests/hooks/test_server_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,12 +911,7 @@ def test_pre_tool_allow_bypasses_hitl_and_preserves_context(
handler.assert_called_once_with(request)


def test_server_pre_tool_node_runs_before_stock_hitl(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from deepagents_code._env_vars import EXPERIMENTAL

monkeypatch.setenv(EXPERIMENTAL, "1")
def test_server_pre_tool_node_runs_before_stock_hitl(tmp_path: Path) -> None:
model = GenericFakeChatModel(messages=iter([AIMessage(content="done")]))
model.profile = {"max_input_tokens": 200000}
graph, _backend = create_cli_agent(
Expand Down
7 changes: 0 additions & 7 deletions libs/code/tests/unit_tests/hooks/test_trust.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import pytest

from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.approval_mode import ApprovalMode
from deepagents_code.hooks.manager import HookSessionIdentity, HooksManager
from deepagents_code.hooks.models.domain import (
Expand All @@ -33,12 +32,6 @@
from deepagents_code.app import DeepAgentsApp


@pytest.fixture(autouse=True)
def _enable_hooks_v2(monkeypatch: pytest.MonkeyPatch) -> None:
"""Hooks v2 only loads in experimental mode, which these tests exercise."""
monkeypatch.setenv(EXPERIMENTAL, "1")


def _write_project_hooks(root: Path, *, event: str = "Stop") -> Path:
(root / ".git").mkdir(parents=True, exist_ok=True)
hooks_dir = root / ".deepagents"
Expand Down
2 changes: 0 additions & 2 deletions libs/code/tests/unit_tests/plugins/test_plugin_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import pytest

from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.approval_mode import ApprovalMode
from deepagents_code.hooks.manager import HookSessionIdentity, HooksManager
from deepagents_code.hooks.models.domain import HookEvent, SessionStartCause
Expand Down Expand Up @@ -39,7 +38,6 @@ def _stage_plugins(
monkeypatch: pytest.MonkeyPatch,
documents: Mapping[str, dict[str, object] | bytes],
) -> tuple[Path, Path]:
monkeypatch.setenv(EXPERIMENTAL, "1")
user_dir = tmp_path / "config"
user_dir.mkdir(parents=True, exist_ok=True)
for module in ("model_config", "hooks.loading", "hooks.runtime"):
Expand Down
7 changes: 1 addition & 6 deletions libs/code/tests/unit_tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3544,21 +3544,19 @@ def test_adds_configurable_model_middleware_to_implicit_model_subagents(
), f"Unexpected shell middleware on subagent {name!r}"

def test_subagent_middleware_combines_shell_configurable_model_and_cost(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
self, tmp_path: Path
) -> None:
"""Restrictive shell + implicit model should yield shell, model, and cost.

Explicitly pinned subagents keep shell restriction and cost tracking but
must not gain `ConfigurableModelMiddleware`, which would let a runtime
`/model` switch clobber the pinned model.
"""
from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.agent import ShellAllowListMiddleware
from deepagents_code.configurable_model import ConfigurableModelMiddleware
from deepagents_code.cost_tracking import CostTrackingMiddleware
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware

monkeypatch.setenv(EXPERIMENTAL, "1")
mock_settings = self._build_mock_settings(tmp_path)
mock_agent = Mock()
mock_agent.with_config.return_value = mock_agent
Expand Down Expand Up @@ -4957,7 +4955,6 @@ def test_auto_classifier_model_defaults_to_inheriting(self, tmp_path: Path) -> N
def test_single_hitl_slot_precedes_server_hooks(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
*,
auto_mode_enabled: bool,
) -> None:
Expand All @@ -4969,10 +4966,8 @@ def test_single_hitl_slot_precedes_server_hooks(
stay behind whichever one is installed so its `after_model` `PreToolUse`
pass resolves before approval routing.
"""
from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.hooks.server_middleware import ServerHooksMiddleware

monkeypatch.setenv(EXPERIMENTAL, "1")
middleware = self._capture_middleware(
tmp_path, auto_mode_enabled=auto_mode_enabled
)
Expand Down
17 changes: 2 additions & 15 deletions libs/code/tests/unit_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27660,15 +27660,9 @@ async def test_toggle_off_while_reconnecting_stages_manual(self) -> None:
assert app._session_state.approval_mode is ApprovalMode.MANUAL
assert app._approval_mode_blocked is False

async def test_session_init_keeps_mode_changed_during_construction(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
from deepagents_code._env_vars import EXPERIMENTAL
async def test_session_init_keeps_mode_changed_during_construction(self) -> None:
from deepagents_code.approval_mode import ApprovalMode

# `HooksRuntime.create` is the construction seam probed below, and it
# only runs in experimental mode.
monkeypatch.setenv(EXPERIMENTAL, "1")
app = DeepAgentsApp(approval_mode=ApprovalMode.MANUAL)

def change_mode_during_construction(**_kwargs: object) -> None:
Expand All @@ -27683,19 +27677,12 @@ def change_mode_during_construction(**_kwargs: object) -> None:
assert app._session_state is not None
assert app._session_state.approval_mode is ApprovalMode.AUTO

async def test_session_init_builds_one_state_for_concurrent_callers(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_session_init_builds_one_state_for_concurrent_callers(self) -> None:
"""The startup worker and the inline startup fallback must not race.

Idempotency rests on construction staying free of `await`; reintroducing
one would let both callers pass the guard and build two states.
"""
from deepagents_code._env_vars import EXPERIMENTAL

# `HooksRuntime.create` is the construction seam counted below, and it
# only runs in experimental mode.
monkeypatch.setenv(EXPERIMENTAL, "1")
app = DeepAgentsApp()
creations = 0

Expand Down
7 changes: 1 addition & 6 deletions libs/code/tests/unit_tests/test_non_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from rich.style import Style
from rich.text import Text

from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code._tool_stream import (
TOOL_OUTPUT_TRUNCATION_MARKER,
UNRENDERABLE_TOOL_OUTPUT,
Expand Down Expand Up @@ -381,11 +380,8 @@ async def test_sandbox_type_passed_to_server(self) -> None:
assert kwargs["profile_overrides"] == {"max_input_tokens": 32_000}
assert kwargs["enable_interpreter"] is None

async def test_permission_hooks_override_headless_yolo_bypass(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
async def test_permission_hooks_override_headless_yolo_bypass(self) -> None:
"""Permission hooks force client resolution while retaining YOLO context."""
monkeypatch.setenv(EXPERIMENTAL, "1")
runtime = MagicMock()
runtime.configured_events.return_value = frozenset(
{HookEvent.PERMISSION_REQUEST}
Expand Down Expand Up @@ -1679,7 +1675,6 @@ async def test_run_agent_loop_trusts_project_hooks_when_opted_in(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""`--trust-project-hooks` loads repository hook handlers."""
monkeypatch.setenv(EXPERIMENTAL, "1")
monkeypatch.chdir(tmp_path)
project_hooks = tmp_path / ".deepagents"
project_hooks.mkdir()
Expand Down