Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3d16e21
feat(code): integrate Hooks v2 server lifecycle events
cursoragent Jul 23, 2026
3c4a411
style(code): format HooksSnapshot configured_events
cursoragent Jul 23, 2026
34dd24c
fix(code): satisfy hooks server-lifecycle type checks
cursoragent Jul 23, 2026
8172f40
fix(code): move CLIContext import behind TYPE_CHECKING
cursoragent Jul 23, 2026
54c2175
fix(code): align server hooks lifecycle with design-doc MVP
johannes117 Jul 23, 2026
46dc9ba
fix(code): harden server hooks lifecycle review findings
johannes117 Jul 24, 2026
e7eb344
test(code): expect ServerHooksMiddleware on subagent stacks
johannes117 Jul 24, 2026
8a15a16
feat(code): integrate Hooks v2 client lifecycle events
johannes117 Jul 24, 2026
2d32caf
fix(code): avoid session-start deadlock on hooks init
johannes117 Jul 24, 2026
7b4a4eb
fix(code): keep hook transcripts in the global config dir
johannes117 Jul 24, 2026
3020826
fix(code): initialize session hooks state synchronously
johannes117 Jul 24, 2026
8c0c94c
fix(code): make Hooks v2 lifecycle replay-safe
johannes117 Jul 24, 2026
cdd186b
Merge branch 'johannes/code/dcd-69-server-lifecycle-f6bb' into johann…
johannes117 Jul 24, 2026
1f00788
fix(code): complete Hooks v2 client lifecycle wiring
johannes117 Jul 24, 2026
06cb318
Merge branch 'main' into johannes/code/dcd-69-server-lifecycle-f6bb
johannes117 Jul 24, 2026
25105e1
Merge branch 'johannes/code/dcd-69-server-lifecycle-f6bb' into johann…
johannes117 Jul 24, 2026
6b3b52c
cr
johannes117 Jul 24, 2026
0041764
Merge branch 'main' into johannes/code/dcd-69-server-lifecycle-f6bb
johannes117 Jul 24, 2026
fb16ca9
Merge branch 'johannes/code/dcd-69-server-lifecycle-f6bb' into johann…
johannes117 Jul 24, 2026
9943428
Merge branch 'main' into johannes/code/dcd-69-server-lifecycle-f6bb
open-swe Jul 27, 2026
15c5588
Merge branch 'johannes/code/dcd-69-server-lifecycle-f6bb' into johann…
open-swe Jul 27, 2026
29fbb43
Merge branch 'main' into johannes/code/dcd-69-server-lifecycle-f6bb
open-swe Jul 27, 2026
f4e8ee6
Merge branch 'johannes/code/dcd-69-server-lifecycle-f6bb' into johann…
open-swe Jul 27, 2026
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
23 changes: 23 additions & 0 deletions libs/code/deepagents_code/_cli_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ class CLIContextSchema:

offload_tool_call_id: str | None = None

hooks_snapshot_id: str | None = None

hooks_server_events: list[str] = field(default_factory=list)

prompt_id: str | None = None


class CLIContext(TypedDict, total=False):
"""Client-facing builder for the per-run graph context payload.
Expand Down Expand Up @@ -107,3 +113,20 @@ class CLIContext(TypedDict, total=False):
This is set by the client, not graph state, so model-generated calls cannot
grant themselves permission to execute during the hidden compaction turn.
"""

hooks_snapshot_id: str | None
"""Canonical Hooks v2 configuration hash for this session.

Server-owned lifecycle middleware includes this id on interrupt requests so
the client can reject mismatched resumes.
"""

hooks_server_events: list[str]
"""Server-owned HookEvent names that have configured handlers.

Middleware only interrupts for events listed here, avoiding a round-trip
when the session snapshot has no matching handlers.
"""

prompt_id: str | None
"""Optional per-turn prompt id projected into hook context."""
65 changes: 48 additions & 17 deletions libs/code/deepagents_code/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,14 @@ def _should_interrupt_tool_call(
Returns:
`True` to interrupt, or `False` for Auto/YOLO bypass.
"""
from deepagents_code.hooks.server_middleware import pre_tool_behavior

tool_call = getattr(request, "tool_call", None)
tool_call_id = str(tool_call.get("id") or "") if isinstance(tool_call, dict) else ""
hook_behavior = pre_tool_behavior(getattr(request, "state", None), tool_call_id)
if hook_behavior in {"allow", "deny"}:
return False

runtime = getattr(request, "runtime", None)
mode = _async_routing_mode(getattr(request, "state", None))
if mode is None:
Expand Down Expand Up @@ -2447,6 +2455,20 @@ def _subagent_cli_middleware(
middleware.append(_GlmTerminalStallRecovery())
if restrictive_shell_allow_list is not None:
middleware.append(ShellAllowListMiddleware(restrictive_shell_allow_list))
# 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`).
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,
)
)
# 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 @@ -2741,24 +2763,19 @@ def _subagent_cli_middleware(
fs_tools=fs_tools,
)

interrupt_on: dict[str, bool | InterruptOnConfig] | None
interrupt_on: dict[str, bool | InterruptOnConfig] = {}
auto_mode_config: tuple[Path, list[str]] | None = None
if resolved_interrupt_on is None:
interrupt_on = {}
else:
interrupt_on = resolved_interrupt_on # ty: ignore[invalid-assignment] # InterruptOnConfig is compatible at runtime
if auto_mode_enabled:
configured_allow_list = shell_allow_list or settings.shell_allow_list
narrow_allow_list = (
configured_allow_list if isinstance(configured_allow_list, list) else []
)
trusted_root = (
project_context.project_root
if project_context is not None
and project_context.project_root is not None
else effective_cwd or Path.cwd()
)
auto_mode_config = (Path(trusted_root), narrow_allow_list)
if resolved_interrupt_on is not None and auto_mode_enabled:
configured_allow_list = shell_allow_list or settings.shell_allow_list
narrow_allow_list = (
configured_allow_list if isinstance(configured_allow_list, list) else []
)
trusted_root = (
project_context.project_root
if project_context is not None and project_context.project_root is not None
else effective_cwd or Path.cwd()
)
auto_mode_config = (Path(trusted_root), narrow_allow_list)

# Set up composite backend with routing.
if sandbox is None:
Expand Down Expand Up @@ -2818,6 +2835,20 @@ def _subagent_cli_middleware(
trusted_compaction_tool=compaction_middleware.tools[0],
)
)
elif resolved_interrupt_on is not None:
# `AutoModeHITLMiddleware` reports the same `HumanInTheLoopMiddleware`
# name, so installing both would trip `create_agent`'s duplicate-name
# assertion. Auto mode's specialized replacement wins when active.
agent_middleware.append(AsyncApprovalHITLMiddleware(resolved_interrupt_on))

# Server-owned Hooks v2 lifecycle events (Pre/Post tool, Stop, subagent).
# 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

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
Loading