|
1 | 1 | """Agent management and creation for the CLI.""" |
2 | 2 |
|
3 | 3 | import os |
| 4 | +import re |
4 | 5 | import shutil |
5 | 6 | from pathlib import Path |
| 7 | +from functools import reduce |
| 8 | +from itertools import chain |
| 9 | +from typing import Any |
6 | 10 |
|
7 | 11 | from deepagents import create_deep_agent |
8 | 12 | from deepagents.backends import CompositeBackend |
@@ -57,6 +61,14 @@ def list_agents() -> None: |
57 | 61 | console.print() |
58 | 62 |
|
59 | 63 |
|
| 64 | +def list_assistant_ids() -> list: |
| 65 | + agents_dir = settings.user_deepagents_dir |
| 66 | + project_deepagents_dir = settings.ensure_project_deepagents_dir() |
| 67 | + if not agents_dir.exists() or not any(chain(agents_dir.iterdir(), project_deepagents_dir.iterdir())): |
| 68 | + return [] |
| 69 | + return reduce(lambda agent_paths, agent_path: [*agent_paths, agent_path.name] if agent_path.is_dir() and (agent_path / "agent.md").exists() else agent_paths, sorted(chain(agents_dir.iterdir(), project_deepagents_dir.iterdir())), []) |
| 70 | + |
| 71 | + |
60 | 72 | def reset_agent(agent_name: str, source_agent: str | None = None) -> None: |
61 | 73 | """Reset an agent to default or copy from another agent.""" |
62 | 74 | agents_dir = settings.user_deepagents_dir |
@@ -322,6 +334,89 @@ def _add_interrupt_on() -> dict[str, InterruptOnConfig]: |
322 | 334 | "task": task_interrupt_config, |
323 | 335 | } |
324 | 336 |
|
| 337 | +def create_subagent_with_config( |
| 338 | + model: str | BaseChatModel, |
| 339 | + assistant_id: str, |
| 340 | + tools: list[BaseTool], |
| 341 | + *, |
| 342 | + sandbox: SandboxBackendProtocol | None = None, |
| 343 | + sandbox_type: str | None = None, |
| 344 | +) -> dict[str, Any]: |
| 345 | + """Create and configure an agent with the specified model and tools. |
| 346 | +
|
| 347 | + Args: |
| 348 | + model: LLM model to use |
| 349 | + assistant_id: Agent identifier for memory storage |
| 350 | + tools: Additional tools to provide to agent |
| 351 | + sandbox: Optional sandbox backend for remote execution (e.g., ModalBackend). |
| 352 | + If None, uses local filesystem + shell. |
| 353 | + sandbox_type: Type of sandbox provider ("modal", "runloop", "daytona") |
| 354 | +
|
| 355 | + Returns: |
| 356 | + subagent dict object |
| 357 | + """ |
| 358 | + # Setup agent directory for persistent memory (same for both local and remote modes) |
| 359 | + agent_dir = settings.ensure_agent_dir(assistant_id) |
| 360 | + agent_md = agent_dir / "agent.md" |
| 361 | + if not agent_md.exists(): |
| 362 | + source_content = get_default_coding_instructions() |
| 363 | + agent_md.write_text(source_content) |
| 364 | + |
| 365 | + # Skills directory - per-agent (user-level) |
| 366 | + skills_dir = settings.ensure_user_skills_dir(assistant_id) |
| 367 | + |
| 368 | + # Project-level skills directory (if in a project) |
| 369 | + project_skills_dir = settings.get_project_skills_dir() |
| 370 | + |
| 371 | + # CONDITIONAL SETUP: Local vs Remote Sandbox |
| 372 | + if sandbox is None: |
| 373 | + # Middleware: AgentMemoryMiddleware, SkillsMiddleware, ShellToolMiddleware |
| 374 | + agent_middleware = [ |
| 375 | + AgentMemoryMiddleware(settings=settings, assistant_id=assistant_id), |
| 376 | + SkillsMiddleware( |
| 377 | + skills_dir=skills_dir, |
| 378 | + assistant_id=assistant_id, |
| 379 | + project_skills_dir=project_skills_dir, |
| 380 | + ), |
| 381 | + ShellMiddleware( |
| 382 | + workspace_root=str(Path.cwd()), |
| 383 | + env=os.environ, |
| 384 | + ), |
| 385 | + ] |
| 386 | + else: |
| 387 | + # Middleware: AgentMemoryMiddleware and SkillsMiddleware |
| 388 | + # NOTE: File operations (ls, read, write, edit, glob, grep) and execute tool |
| 389 | + # are automatically provided by create_deep_agent when backend is a SandboxBackend. |
| 390 | + agent_middleware = [ |
| 391 | + AgentMemoryMiddleware(settings=settings, assistant_id=assistant_id), |
| 392 | + SkillsMiddleware( |
| 393 | + skills_dir=skills_dir, |
| 394 | + assistant_id=assistant_id, |
| 395 | + project_skills_dir=project_skills_dir, |
| 396 | + ), |
| 397 | + ] |
| 398 | + |
| 399 | + # Get the system prompt (sandbox-aware and with skills) |
| 400 | + system_prompt = get_system_prompt(assistant_id=assistant_id, sandbox_type=sandbox_type) |
| 401 | + |
| 402 | + interrupt_on = _add_interrupt_on() |
| 403 | + |
| 404 | + content = agent_md.read_text() |
| 405 | + match1 = re.search(r"model: ([^\n]*)", content) # allow subagent to dynamically switch model |
| 406 | + model = match1 and match1.group(1) or model |
| 407 | + description = content.split("#")[0].replace(f"model: {model}", "").strip() |
| 408 | + agent = { |
| 409 | + "name": assistant_id, |
| 410 | + "description": description, |
| 411 | + "model": model, |
| 412 | + "system_prompt": system_prompt, |
| 413 | + "tools": tools, |
| 414 | + "middleware": agent_middleware, |
| 415 | + "interrupt_on": interrupt_on, |
| 416 | + } |
| 417 | + |
| 418 | + return agent |
| 419 | + |
325 | 420 |
|
326 | 421 | def create_agent_with_config( |
327 | 422 | model: str | BaseChatModel, |
@@ -404,13 +499,19 @@ def create_agent_with_config( |
404 | 499 |
|
405 | 500 | interrupt_on = _add_interrupt_on() |
406 | 501 |
|
| 502 | + subagents = [] |
| 503 | + for assistant_id in [item for item in list_assistant_ids() if item != assistant_id]: |
| 504 | + subagent = create_subagent_with_config(model, assistant_id, tools, sandbox=sandbox, sandbox_type=sandbox_type) |
| 505 | + subagents.append(subagent) |
| 506 | + |
407 | 507 | agent = create_deep_agent( |
408 | 508 | model=model, |
409 | 509 | system_prompt=system_prompt, |
410 | 510 | tools=tools, |
411 | 511 | backend=composite_backend, |
412 | 512 | middleware=agent_middleware, |
413 | 513 | interrupt_on=interrupt_on, |
| 514 | + subagents=subagents |
414 | 515 | ).with_config(config) |
415 | 516 |
|
416 | 517 | agent.checkpointer = InMemorySaver() |
|
0 commit comments