Skip to content
139 changes: 139 additions & 0 deletions libs/code/deepagents_code/acp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Dcode-specific ACP approval-mode adapter."""

from __future__ import annotations

from contextvars import ContextVar
from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4

from acp.schema import PromptResponse, TextContentBlock
from deepagents_acp.server import AgentServerACP as BaseAgentServerACP
from langchain_core.messages import HumanMessage

from deepagents_code._cli_context import CLIContextSchema
from deepagents_code.approval_mode import (
APPROVAL_MODE_NAMESPACE,
ApprovalMode,
approval_mode_key,
approval_mode_payload,
)
from deepagents_code.auto_mode import USER_PROMPT_METADATA_KEY, user_prompt_metadata

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Callable, Sequence

from deepagents_acp.server import AgentSessionContext
from langchain_core.runnables import RunnableConfig
from langgraph.pregel import Pregel
from langgraph.store.base import BaseStore
from langgraph.types import Command

_prompt: ContextVar[str | None] = ContextVar("acp_auto_prompt", default=None)


class _AutoGraph:
"""Add trusted Auto control state to ACP graph runs."""

def __init__(self, graph: Pregel[Any, Any, Any, Any], store: BaseStore) -> None:
self._graph = graph
self._store = store
self.checkpointer = graph.checkpointer
self._turn_id = ""

async def astream(
self,
value: dict[str, Any] | Command,
*,
config: RunnableConfig,
**kwargs: Any,
) -> AsyncIterator[Any]:
session_id = config["configurable"]["thread_id"]
prompt = _prompt.get()
if prompt is not None:
self._turn_id = uuid4().hex
key = approval_mode_key(session_id)
self._store.put(
APPROVAL_MODE_NAMESPACE,
key,
dict(approval_mode_payload(mode=ApprovalMode.AUTO)),
)
if prompt is not None and isinstance(value, dict):
messages = list(value.get("messages", []))
if messages:
messages[-1] = HumanMessage(
content=messages[-1]["content"],
additional_kwargs={
USER_PROMPT_METADATA_KEY: user_prompt_metadata(
prompt, [], turn_id=self._turn_id
)
},
)
value = {**value, "messages": messages}
context = CLIContextSchema(
approval_mode=ApprovalMode.AUTO.value,
auto_approve=True,
approval_mode_key=key,
thread_id=session_id,
turn_id=self._turn_id,
)
graph = cast("Any", self._graph)
async for chunk in graph.astream(
value, config=config, context=context, **kwargs
):
yield chunk

async def aget_state(self, config: RunnableConfig) -> object:
return await self._graph.aget_state(config)

async def aupdate_state(
self,
config: RunnableConfig,
values: dict[str, Any],
*,
as_node: str | None = None,
) -> RunnableConfig:
return await self._graph.aupdate_state(config, values, as_node=as_node)

def aget_state_history(self, config: RunnableConfig) -> AsyncIterator[Any]:
return self._graph.aget_state_history(config)


class AgentServerACP(BaseAgentServerACP):
"""ACP server that supplies trusted classifier context in Auto mode."""

def __init__(
self,
agent: Callable[[AgentSessionContext], Pregel[Any, Any, Any, Any]],
*,
store: BaseStore,
**kwargs: Any,
) -> None:
"""Initialize the Auto-aware ACP server."""

def build(context: AgentSessionContext) -> _AutoGraph:
return _AutoGraph(agent(context), store)

super().__init__(cast("Any", build), **kwargs)

async def prompt(
self,
prompt: Sequence[Any],
session_id: str,
message_id: str | None = None,
**kwargs: Any,
) -> PromptResponse:
"""Run an ACP prompt with trusted classifier metadata.

Returns:
The ACP prompt response.
"""
text = "\n".join(
block.text for block in prompt if isinstance(block, TextContentBlock)
)
token = _prompt.set(text)
try:
return await super().prompt(
list(prompt), session_id, message_id=message_id, **kwargs
)
finally:
_prompt.reset(token)
13 changes: 8 additions & 5 deletions libs/code/deepagents_code/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.pregel import Pregel
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore
from langgraph.types import Command

from deepagents_code.mcp_tools import MCPServerInfo
Expand Down Expand Up @@ -2203,6 +2204,7 @@ def create_cli_agent(
auto_classifier_model: str | BaseChatModel | None = None,
recursion_limit: int | None = None,
checkpointer: BaseCheckpointSaver | None = None,
store: BaseStore | None = None,
mcp_server_info: list[MCPServerInfo] | None = None,
cwd: str | Path | None = None,
project_context: ProjectContext | None = None,
Expand Down Expand Up @@ -2254,8 +2256,8 @@ def create_cli_agent(

If `False`, tools pause for user confirmation via the approval menu.
See `_add_interrupt_on` for the full list of gated tools.
auto_mode_enabled: Install classifier-backed Auto for the local Textual
runtime. Callers must leave this disabled for headless, remote, and
auto_mode_enabled: Install classifier-backed Auto for local TUI or ACP
runtimes. Callers must leave this disabled for headless and
sandbox-backed graphs.
interrupt_shell_only: If `True`, all HITL interrupts are disabled;
shell commands are validated inline by `ShellAllowListMiddleware`
Expand Down Expand Up @@ -2342,6 +2344,7 @@ def create_cli_agent(
in `config.toml`, then the default via `resolve_recursion_limit`.
checkpointer: Optional checkpointer for session persistence.
When `None`, the graph is compiled without a checkpointer.
store: Optional LangGraph Store for runtime approval state.
mcp_server_info: MCP server metadata to surface in the system prompt.
cwd: Override the working directory for the agent's filesystem backend
and system prompt.
Expand Down Expand Up @@ -2372,10 +2375,9 @@ def create_cli_agent(
"""
tools = tools or []
mcp_tools = tuple(mcp_tools or ())
if auto_mode_enabled and (not interactive or sandbox is not None):
if auto_mode_enabled and sandbox is not None:
logger.warning(
"Classifier-backed Auto is unavailable outside the local interactive "
"runtime; using Manual HITL"
"Classifier-backed Auto is unavailable with a sandbox; using Manual HITL"
)
auto_mode_enabled = False
effective_cwd = (
Expand Down Expand Up @@ -3071,6 +3073,7 @@ def _subagent_cli_middleware(
interrupt_on=interrupt_on,
context_schema=CLIContextSchema,
checkpointer=checkpointer,
store=store,
subagents=all_subagents or None,
name=_sanitize_agent_message_name(assistant_id),
).with_config({**config, "recursion_limit": effective_recursion_limit})
Expand Down
4 changes: 2 additions & 2 deletions libs/code/deepagents_code/auto_mode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Classifier-backed approval policy for the local interactive TUI."""
"""Classifier-backed approval policy for local TUI and ACP runtimes."""

from __future__ import annotations

Expand Down Expand Up @@ -1815,7 +1815,7 @@ def __init__(
trusted_ask_user_tool: BaseTool | None = None,
trusted_compaction_tool: BaseTool | None = None,
) -> None:
"""Initialize the local interactive Auto policy.
"""Initialize the local Auto policy.

Args:
interrupt_on: Shared Manual interrupt map.
Expand Down
58 changes: 44 additions & 14 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2090,7 +2090,7 @@ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]:
dest="auto_classifier_model",
metavar="MODEL",
help="Model the Auto approval classifier reviews actions with "
"(e.g. anthropic:claude-haiku-4-5). Interactive TUI only. Defaults to "
"(e.g. anthropic:claude-haiku-4-5). Local TUI or ACP only. Defaults to "
"DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL, then [models].auto_classifier, "
"then the main agent model. A weaker model weakens Auto's review.",
)
Expand Down Expand Up @@ -2240,14 +2240,14 @@ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]:
"--auto-approve",
action="store_true",
default=None,
help="Interactive local TUI only: enable classifier-backed Auto mode.",
help="Enable classifier-backed Auto mode in the local TUI or ACP server.",
)
approval_group.add_argument(
"--yolo",
action="store_true",
help=(
"Interactive mode only: run gated actions without review after the "
"one-time local risk acknowledgement."
"Run gated actions without review after the one-time local risk "
"acknowledgement (interactive TUI or ACP mode)."
),
)

Expand Down Expand Up @@ -2848,6 +2848,9 @@ async def _run_acp_cli_async(
trust_project_mcp: bool | None = None,
allow_fs_tools: "list[FsToolName] | None" = None,
recursion_limit: int | None = None,
auto: bool = False,
yolo: bool = False,
auto_classifier_model: str | None = None,
) -> int:
"""Run ACP server mode and return a process exit code.

Expand All @@ -2868,6 +2871,9 @@ async def _run_acp_cli_async(
`None` leaves the SDK default (all tools).
recursion_limit: Explicit main-agent `recursion_limit`; `None` resolves
from env/`config.toml`/default at agent-build time.
auto: Enable classifier-backed approval routing.
yolo: Disable approval prompts for this ACP server.
auto_classifier_model: Optional model for Auto approval classification.

Returns:
Exit code for ACP mode.
Expand Down Expand Up @@ -2972,6 +2978,9 @@ async def _run_acp_cli_async(

async with get_checkpointer() as checkpointer:
await checkpointer.setup()
from langgraph.store.memory import InMemoryStore

store = InMemoryStore() if auto else None

def build_agent(
context: "AgentSessionContext",
Expand All @@ -2996,16 +3005,29 @@ def build_agent(
async_subagents=async_subagents,
fs_tools=allow_fs_tools,
recursion_limit=recursion_limit,
auto_approve=yolo,
auto_mode_enabled=auto,
auto_classifier_model=auto_classifier_model,
memory_auto_save=is_memory_auto_save_enabled(),
store=store,
cwd=context.cwd,
project_context=ProjectContext.from_user_cwd(Path(context.cwd)),
)
return agent_graph

server = agent_server_cls(
if auto:
from deepagents_code.acp import AgentServerACP

server_cls = AgentServerACP
server_kwargs = {"store": cast("Any", store)}
else:
server_cls = agent_server_cls
server_kwargs = {}
server = server_cls(
build_agent,
models=models,
load_sessions=True,
**server_kwargs,
)
await run_acp_agent(server)
except KeyboardInterrupt:
Expand Down Expand Up @@ -4285,16 +4307,21 @@ def cli_main() -> None:
sys.exit(1)

if getattr(args, "acp", False):
if getattr(args, "auto_approve", False) or getattr(args, "yolo", False):
flag = "--yolo" if getattr(args, "yolo", False) else "--auto-approve"
sys.stderr.write(
f"Error: {flag} is only supported by the interactive Textual TUI.\n"
)
sys.exit(2)
if getattr(args, "auto_classifier_model", None) is not None:
if getattr(args, "yolo", False):
from deepagents_code.approval_mode import has_yolo_acknowledgement

if not has_yolo_acknowledgement():
sys.stderr.write(
"Error: acknowledge YOLO in the interactive TUI before "
"using it in ACP mode.\n"
)
sys.exit(2)
if getattr(args, "auto_classifier_model", None) is not None and not getattr(
args, "auto_approve", False
):
sys.stderr.write(
"Error: --auto-classifier-model is only supported by the "
"interactive Textual TUI.\n"
"Error: --auto-classifier-model requires --auto-approve "
"in ACP mode.\n"
)
sys.exit(2)
assistant_id = _resolve_agent_arg(args)
Expand Down Expand Up @@ -4335,6 +4362,9 @@ def cli_main() -> None:
trust_project_mcp=getattr(args, "trust_project_mcp", False),
allow_fs_tools=allow_fs_tools,
recursion_limit=getattr(args, "recursion_limit", None),
auto=getattr(args, "auto_approve", False),
yolo=getattr(args, "yolo", False),
auto_classifier_model=getattr(args, "auto_classifier_model", None),
)
)
sys.exit(exit_code)
Expand Down
8 changes: 5 additions & 3 deletions libs/code/deepagents_code/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,20 @@ def show_help() -> None:
console.print(
" --startup-cmd CMD Shell command to run at startup, before first prompt" # noqa: E501
)
console.print(" -y, --auto-approve Enable classifier-backed Auto mode")
console.print(
" -y, --auto-approve Enable classifier-backed Auto mode (TUI or ACP)"
)
console.print(" --auto-classifier-model MODEL")
console.print(
" Model the Auto classifier reviews actions with"
)
console.print(
" Interactive TUI only; defaults to the "
" Local TUI or ACP only; defaults to the "
"main agent model"
)
console.print(
" --yolo Run gated actions without review after "
"acknowledgement"
"acknowledgement (TUI or ACP)"
)
console.print(" --sandbox TYPE Remote sandbox for execution")
console.print(
Expand Down
Loading