Files
deepagent/deepagents_sourcecode/libs/deepagents-cli/deepagents_cli/agent.py
HyunjunJeon af5fbfabec 문서 추가: Context Engineering 문서 추가 및 deepagents_sourcecode 한국어 번역
- Context_Engineering.md: 에이전트 컨텍스트 엔지니어링 개념 정리 문서 추가
- Context_Engineering_Research.ipynb: 연구 노트북 업데이트
- deepagents_sourcecode/: docstring과 주석을 한국어로 번역
2026-01-11 17:55:52 +09:00

469 lines
18 KiB
Python

"""deepagents-cli에서 에이전트를 생성하고 관리하는 로직입니다."""
# ruff: noqa: E501
import os
import shutil
from pathlib import Path
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend
from deepagents.backends.filesystem import FilesystemBackend
from deepagents.backends.sandbox import SandboxBackendProtocol
from deepagents.middleware import MemoryMiddleware, SkillsMiddleware
from langchain.agents.middleware import (
InterruptOnConfig,
)
from langchain.agents.middleware.types import AgentState
from langchain.messages import ToolCall
from langchain.tools import BaseTool
from langchain_core.language_models import BaseChatModel
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.pregel import Pregel
from langgraph.runtime import Runtime
from deepagents_cli.config import COLORS, config, console, get_default_coding_instructions, settings
from deepagents_cli.integrations.sandbox_factory import get_default_working_dir
from deepagents_cli.shell import ShellMiddleware
DESCRIPTION_PREVIEW_LIMIT = 500
def list_agents() -> None:
"""사용 가능한 모든 에이전트를 나열합니다."""
agents_dir = settings.user_deepagents_dir
if not agents_dir.exists() or not any(agents_dir.iterdir()):
console.print("[yellow]No agents found.[/yellow]")
console.print(
"[dim]Agents will be created in ~/.deepagents/ when you first use them.[/dim]",
style=COLORS["dim"],
)
return
console.print("\n[bold]Available Agents:[/bold]\n", style=COLORS["primary"])
for agent_path in sorted(agents_dir.iterdir()):
if agent_path.is_dir():
agent_name = agent_path.name
agent_md = agent_path / "AGENTS.md"
if agent_md.exists():
console.print(f" • [bold]{agent_name}[/bold]", style=COLORS["primary"])
console.print(f" {agent_path}", style=COLORS["dim"])
else:
console.print(
f" • [bold]{agent_name}[/bold] [dim](incomplete)[/dim]", style=COLORS["tool"]
)
console.print(f" {agent_path}", style=COLORS["dim"])
console.print()
def reset_agent(agent_name: str, source_agent: str | None = None) -> None:
"""에이전트를 기본값으로 리셋하거나 다른 에이전트 설정을 복사합니다."""
agents_dir = settings.user_deepagents_dir
agent_dir = agents_dir / agent_name
if source_agent:
source_dir = agents_dir / source_agent
source_md = source_dir / "AGENTS.md"
if not source_md.exists():
console.print(
f"[bold red]Error:[/bold red] Source agent '{source_agent}' not found "
"or has no AGENTS.md"
)
return
source_content = source_md.read_text()
action_desc = f"contents of agent '{source_agent}'"
else:
source_content = get_default_coding_instructions()
action_desc = "default"
if agent_dir.exists():
shutil.rmtree(agent_dir)
console.print(f"Removed existing agent directory: {agent_dir}", style=COLORS["tool"])
agent_dir.mkdir(parents=True, exist_ok=True)
agent_md = agent_dir / "AGENTS.md"
agent_md.write_text(source_content)
console.print(f"✓ Agent '{agent_name}' reset to {action_desc}", style=COLORS["primary"])
console.print(f"Location: {agent_dir}\n", style=COLORS["dim"])
def get_system_prompt(assistant_id: str, sandbox_type: str | None = None) -> str:
"""에이전트의 기본 system prompt를 생성합니다.
Args:
assistant_id: The agent identifier for path references
sandbox_type: Type of sandbox provider ("modal", "runloop", "daytona").
If None, agent is operating in local mode.
Returns:
The system prompt string (without AGENTS.md content)
"""
agent_dir_path = f"~/.deepagents/{assistant_id}"
if sandbox_type:
# Get provider-specific working directory
working_dir = get_default_working_dir(sandbox_type)
working_dir_section = f"""### Current Working Directory
You are operating in a **remote Linux sandbox** at `{working_dir}`.
All code execution and file operations happen in this sandbox environment.
**Important:**
- The CLI is running locally on the user's machine, but you execute code remotely
- Use `{working_dir}` as your working directory for all operations
"""
else:
cwd = Path.cwd()
working_dir_section = f"""<env>
Working directory: {cwd}
</env>
### Current Working Directory
The filesystem backend is currently operating in: `{cwd}`
### File System and Paths
**IMPORTANT - Path Handling:**
- All file paths must be absolute paths (e.g., `{cwd}/file.txt`)
- Use the working directory from <env> to construct absolute paths
- Example: To create a file in your working directory, use `{cwd}/research_project/file.md`
- Never use relative paths - always construct full absolute paths
"""
return (
working_dir_section
+ f"""### Skills Directory
Your skills are stored at: `{agent_dir_path}/skills/`
Skills may contain scripts or supporting files. When executing skill scripts with bash, use the real filesystem path:
Example: `bash python {agent_dir_path}/skills/web-research/script.py`
### Human-in-the-Loop Tool Approval
Some tool calls require user approval before execution. When a tool call is rejected by the user:
1. Accept their decision immediately - do NOT retry the same command
2. Explain that you understand they rejected the action
3. Suggest an alternative approach or ask for clarification
4. Never attempt the exact same rejected command again
Respect the user's decisions and work with them collaboratively.
### Web Search Tool Usage
When you use the web_search tool:
1. The tool will return search results with titles, URLs, and content excerpts
2. You MUST read and process these results, then respond naturally to the user
3. NEVER show raw JSON or tool results directly to the user
4. Synthesize the information from multiple sources into a coherent answer
5. Cite your sources by mentioning page titles or URLs when relevant
6. If the search doesn't find what you need, explain what you found and ask clarifying questions
The user only sees your text responses - not tool results. Always provide a complete, natural language answer after using web_search.
### Todo List Management
When using the write_todos tool:
1. Keep the todo list MINIMAL - aim for 3-6 items maximum
2. Only create todos for complex, multi-step tasks that truly need tracking
3. Break down work into clear, actionable items without over-fragmenting
4. For simple tasks (1-2 steps), just do them directly without creating todos
5. When first creating a todo list for a task, ALWAYS ask the user if the plan looks good before starting work
- Create the todos, let them render, then ask: "Does this plan look good?" or similar
- Wait for the user's response before marking the first todo as in_progress
- If they want changes, adjust the plan accordingly
6. Update todo status promptly as you complete each item
The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking."""
)
def _format_write_file_description(
tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
"""승인 프롬프트에 표시할 `write_file` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
file_path = args.get("file_path", "unknown")
content = args.get("content", "")
action = "Overwrite" if Path(file_path).exists() else "Create"
line_count = len(content.splitlines())
return f"File: {file_path}\nAction: {action} file\nLines: {line_count}"
def _format_edit_file_description(
tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
"""승인 프롬프트에 표시할 `edit_file` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
file_path = args.get("file_path", "unknown")
replace_all = bool(args.get("replace_all", False))
return (
f"File: {file_path}\n"
f"Action: Replace text ({'all occurrences' if replace_all else 'single occurrence'})"
)
def _format_web_search_description(
tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
"""승인 프롬프트에 표시할 `web_search` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
query = args.get("query", "unknown")
max_results = args.get("max_results", 5)
return f"Query: {query}\nMax results: {max_results}\n\n⚠️ This will use Tavily API credits"
def _format_fetch_url_description(
tool_call: ToolCall, _state: AgentState, _runtime: Runtime
) -> str:
"""승인 프롬프트에 표시할 `fetch_url` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
url = args.get("url", "unknown")
timeout = args.get("timeout", 30)
return f"URL: {url}\nTimeout: {timeout}s\n\n⚠️ Will fetch and convert web content to markdown"
def _format_task_description(tool_call: ToolCall, _state: AgentState, _runtime: Runtime) -> str:
"""승인 프롬프트에 표시할 `task`(서브에이전트) 도구 호출 설명을 포맷팅합니다.
The task tool signature is: task(description: str, subagent_type: str)
The description contains all instructions that will be sent to the subagent.
"""
args = tool_call["args"]
description = args.get("description", "unknown")
subagent_type = args.get("subagent_type", "unknown")
# Truncate description if too long for display
description_preview = description
if len(description) > DESCRIPTION_PREVIEW_LIMIT:
description_preview = description[:DESCRIPTION_PREVIEW_LIMIT] + "..."
return (
f"Subagent Type: {subagent_type}\n\n"
f"Task Instructions:\n"
f"{'' * 40}\n"
f"{description_preview}\n"
f"{'' * 40}\n\n"
f"⚠️ Subagent will have access to file operations and shell commands"
)
def _format_shell_description(tool_call: ToolCall, _state: AgentState, _runtime: Runtime) -> str:
"""승인 프롬프트에 표시할 `shell` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
command = args.get("command", "N/A")
return f"Shell Command: {command}\nWorking Directory: {Path.cwd()}"
def _format_execute_description(tool_call: ToolCall, _state: AgentState, _runtime: Runtime) -> str:
"""승인 프롬프트에 표시할 `execute` 도구 호출 설명을 포맷팅합니다."""
args = tool_call["args"]
command = args.get("command", "N/A")
return f"Execute Command: {command}\nLocation: Remote Sandbox"
def _add_interrupt_on() -> dict[str, InterruptOnConfig]:
"""파괴적인 도구에 대한 HITL(Human-In-The-Loop) interrupt_on 설정을 구성합니다."""
shell_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_shell_description,
}
execute_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_execute_description,
}
write_file_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_write_file_description,
}
edit_file_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_edit_file_description,
}
web_search_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_web_search_description,
}
fetch_url_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_fetch_url_description,
}
task_interrupt_config: InterruptOnConfig = {
"allowed_decisions": ["approve", "reject"],
"description": _format_task_description,
}
return {
"shell": shell_interrupt_config,
"execute": execute_interrupt_config,
"write_file": write_file_interrupt_config,
"edit_file": edit_file_interrupt_config,
"web_search": web_search_interrupt_config,
"fetch_url": fetch_url_interrupt_config,
"task": task_interrupt_config,
}
def create_cli_agent(
model: str | BaseChatModel,
assistant_id: str,
*,
tools: list[BaseTool] | None = None,
sandbox: SandboxBackendProtocol | None = None,
sandbox_type: str | None = None,
system_prompt: str | None = None,
auto_approve: bool = False,
enable_memory: bool = True,
enable_skills: bool = True,
enable_shell: bool = True,
checkpointer: BaseCheckpointSaver | None = None,
) -> tuple[Pregel, CompositeBackend]:
"""옵션을 유연하게 조합할 수 있는 CLI용 에이전트를 생성합니다.
This is the main entry point for creating a deepagents CLI agent, usable both
internally and from external code (e.g., benchmarking frameworks, Harbor).
Args:
model: LLM model to use (e.g., "anthropic:claude-sonnet-4-5-20250929")
assistant_id: Agent identifier for memory/state storage
tools: Additional tools to provide to agent
sandbox: Optional sandbox backend for remote execution (e.g., ModalBackend).
If None, uses local filesystem + shell.
sandbox_type: Type of sandbox provider ("modal", "runloop", "daytona").
Used for system prompt generation.
system_prompt: Override the default system prompt. If None, generates one
based on sandbox_type and assistant_id.
auto_approve: If True, automatically approves all tool calls without human
confirmation. Useful for automated workflows.
enable_memory: Enable MemoryMiddleware for persistent memory
enable_skills: Enable SkillsMiddleware for custom agent skills
enable_shell: Enable ShellMiddleware for local shell execution (only in local mode)
checkpointer: Optional checkpointer for session persistence. If None, uses
InMemorySaver (no persistence across CLI invocations).
Returns:
2-tuple of (agent_graph, backend)
- agent_graph: Configured LangGraph Pregel instance ready for execution
- composite_backend: CompositeBackend for file operations
"""
tools = tools or []
# Setup agent directory for persistent memory (if enabled)
if enable_memory or enable_skills:
agent_dir = settings.ensure_agent_dir(assistant_id)
agent_md = agent_dir / "AGENTS.md"
if not agent_md.exists():
source_content = get_default_coding_instructions()
agent_md.write_text(source_content)
# Skills directories (if enabled)
skills_dir = None
project_skills_dir = None
if enable_skills:
skills_dir = settings.ensure_user_skills_dir(assistant_id)
project_skills_dir = settings.get_project_skills_dir()
# Build middleware stack based on enabled features
agent_middleware = []
# Add memory middleware
if enable_memory:
memory_sources = [str(settings.get_user_agent_md_path(assistant_id))]
project_agent_md = settings.get_project_agent_md_path()
if project_agent_md:
memory_sources.append(str(project_agent_md))
agent_middleware.append(
MemoryMiddleware(
backend=FilesystemBackend(),
sources=memory_sources,
)
)
# Add skills middleware
if enable_skills:
sources = [str(skills_dir)]
if project_skills_dir:
sources.append(str(project_skills_dir))
agent_middleware.append(
SkillsMiddleware(
backend=FilesystemBackend(),
sources=sources,
)
)
# CONDITIONAL SETUP: Local vs Remote Sandbox
if sandbox is None:
# ========== LOCAL MODE ==========
backend = FilesystemBackend() # Current working directory
# Add shell middleware (only in local mode)
if enable_shell:
# Create environment for shell commands
# Restore user's original LANGSMITH_PROJECT so their code traces separately
shell_env = os.environ.copy()
if settings.user_langchain_project:
shell_env["LANGSMITH_PROJECT"] = settings.user_langchain_project
agent_middleware.append(
ShellMiddleware(
workspace_root=str(Path.cwd()),
env=shell_env,
)
)
else:
# ========== REMOTE SANDBOX MODE ==========
backend = sandbox # Remote sandbox (ModalBackend, etc.)
# Note: Shell middleware not used in sandbox mode
# File operations and execute tool are provided by the sandbox backend
# Get or use custom system prompt
if system_prompt is None:
system_prompt = get_system_prompt(assistant_id=assistant_id, sandbox_type=sandbox_type)
# Configure interrupt_on based on auto_approve setting
interrupt_on = {} if auto_approve else _add_interrupt_on()
composite_backend = CompositeBackend(
default=backend,
routes={},
)
# Create the agent
# Use provided checkpointer or fallback to InMemorySaver
final_checkpointer = checkpointer if checkpointer is not None else InMemorySaver()
agent = create_deep_agent(
model=model,
system_prompt=system_prompt,
tools=tools,
backend=composite_backend,
middleware=agent_middleware,
interrupt_on=interrupt_on,
checkpointer=final_checkpointer,
).with_config(config)
return agent, composite_backend