deepy-cli 0.2.16__tar.gz → 0.2.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/PKG-INFO +1 -1
  2. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/pyproject.toml +1 -1
  3. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/__init__.py +1 -1
  4. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/config/__init__.py +4 -0
  5. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/config/settings.py +24 -1
  6. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/apply_patch.md +4 -1
  7. deepy_cli-0.2.18/src/deepy/data/tools/read_file.md +19 -0
  8. deepy_cli-0.2.18/src/deepy/data/tools/test_shell.md +16 -0
  9. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/write_file.md +3 -2
  10. deepy_cli-0.2.18/src/deepy/llm/agent.py +267 -0
  11. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/runner.py +1 -0
  12. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/system.py +9 -0
  13. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/tool_docs.py +1 -0
  14. deepy_cli-0.2.18/src/deepy/subagents.py +323 -0
  15. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/agents.py +339 -36
  16. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/builtin.py +84 -10
  17. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/file_state.py +8 -0
  18. deepy_cli-0.2.18/src/deepy/tools/test_shell.py +529 -0
  19. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/app.py +73 -9
  20. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/widgets.py +45 -4
  21. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/message_view.py +87 -0
  22. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/slash_commands.py +48 -14
  23. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/terminal.py +141 -45
  24. deepy_cli-0.2.16/src/deepy/data/tools/read_file.md +0 -16
  25. deepy_cli-0.2.16/src/deepy/llm/agent.py +0 -61
  26. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/README.md +0 -0
  27. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/__main__.py +0 -0
  28. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/background_tasks.py +0 -0
  29. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/cli.py +0 -0
  30. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/__init__.py +0 -0
  31. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  32. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  33. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  34. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/Search.md +0 -0
  35. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/WebFetch.md +0 -0
  36. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/WebSearch.md +0 -0
  37. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/__init__.py +0 -0
  38. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/edit_text.md +0 -0
  39. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/shell.md +0 -0
  40. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_list.md +0 -0
  41. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_output.md +0 -0
  42. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_stop.md +0 -0
  43. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/todo_write.md +0 -0
  44. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/errors.py +0 -0
  45. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/input_suggestions.py +0 -0
  46. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/__init__.py +0 -0
  47. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/compaction.py +0 -0
  48. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/context.py +0 -0
  49. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/events.py +0 -0
  50. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/model_capabilities.py +0 -0
  51. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/provider.py +0 -0
  52. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/replay.py +0 -0
  53. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/thinking.py +0 -0
  54. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/mcp.py +0 -0
  55. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/__init__.py +0 -0
  56. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/compact.py +0 -0
  57. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/init_agents.py +0 -0
  58. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/rules.py +0 -0
  59. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/runtime_context.py +0 -0
  60. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/session_cost.py +0 -0
  61. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/__init__.py +0 -0
  62. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/jsonl.py +0 -0
  63. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/manager.py +0 -0
  64. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/skill_market.py +0 -0
  65. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/skills.py +0 -0
  66. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/status.py +0 -0
  67. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/todos.py +0 -0
  68. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/__init__.py +0 -0
  69. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/result.py +0 -0
  70. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/search.py +0 -0
  71. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/shell_output.py +0 -0
  72. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/shell_utils.py +0 -0
  73. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/__init__.py +0 -0
  74. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/commands.py +0 -0
  75. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/compat.py +0 -0
  76. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/diff.py +0 -0
  77. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/runner.py +0 -0
  78. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/screens.py +0 -0
  79. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/state.py +0 -0
  80. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/__init__.py +0 -0
  81. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/sdk.py +0 -0
  82. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/tool_payloads.py +0 -0
  83. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/__init__.py +0 -0
  84. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/app.py +0 -0
  85. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/ask_user_question.py +0 -0
  86. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/exit_summary.py +0 -0
  87. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/file_mentions.py +0 -0
  88. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/loading_text.py +0 -0
  89. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/local_command.py +0 -0
  90. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/markdown.py +0 -0
  91. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/model_picker.py +0 -0
  92. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/prompt_buffer.py +0 -0
  93. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/prompt_input.py +0 -0
  94. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/session_list.py +0 -0
  95. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/session_picker.py +0 -0
  96. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/skill_picker.py +0 -0
  97. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/status_footer.py +0 -0
  98. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/styles.py +0 -0
  99. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/theme_picker.py +0 -0
  100. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/thinking_state.py +0 -0
  101. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/welcome.py +0 -0
  102. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/update_check.py +0 -0
  103. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/usage.py +0 -0
  104. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/__init__.py +0 -0
  105. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/debug_logger.py +0 -0
  106. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/error_logger.py +0 -0
  107. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/json.py +0 -0
  108. {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.16"
3
+ version = "0.2.18"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.16"
3
+ __version__ = "0.2.18"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -23,8 +23,10 @@ from .settings import (
23
23
  REASONING_MODES,
24
24
  Settings,
25
25
  SUPPORTED_DEEPSEEK_MODELS,
26
+ TestShellToolConfig,
26
27
  UI_THEME_OPTIONS,
27
28
  UI_THEMES,
29
+ ToolsConfig,
28
30
  UiConfig,
29
31
  XIAOMI_MODEL_CATALOG,
30
32
  allows_custom_model_for_provider,
@@ -79,8 +81,10 @@ __all__ = [
79
81
  "REASONING_MODES",
80
82
  "Settings",
81
83
  "SUPPORTED_DEEPSEEK_MODELS",
84
+ "TestShellToolConfig",
82
85
  "UI_THEME_OPTIONS",
83
86
  "UI_THEMES",
87
+ "ToolsConfig",
84
88
  "UiConfig",
85
89
  "XIAOMI_MODEL_CATALOG",
86
90
  "allows_custom_model_for_provider",
@@ -323,6 +323,12 @@ def _as_str(value: Any, default: str = "") -> str:
323
323
  return value.strip() if isinstance(value, str) and value.strip() else default
324
324
 
325
325
 
326
+ def _as_string_tuple(value: Any) -> tuple[str, ...]:
327
+ if not isinstance(value, list):
328
+ return ()
329
+ return tuple(item.strip() for item in value if isinstance(item, str) and item.strip())
330
+
331
+
326
332
  @dataclass(frozen=True)
327
333
  class ModelConfig:
328
334
  provider: str = DEFAULT_PROVIDER
@@ -468,13 +474,30 @@ class WebSearchToolConfig:
468
474
  )
469
475
 
470
476
 
477
+ @dataclass(frozen=True)
478
+ class TestShellToolConfig:
479
+ allow_patterns: tuple[str, ...] = ()
480
+ approval_required_patterns: tuple[str, ...] = ()
481
+
482
+ @classmethod
483
+ def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
484
+ return cls(
485
+ allow_patterns=_as_string_tuple(raw.get("allow_patterns")),
486
+ approval_required_patterns=_as_string_tuple(raw.get("approval_required_patterns")),
487
+ )
488
+
489
+
471
490
  @dataclass(frozen=True)
472
491
  class ToolsConfig:
473
492
  web_search: WebSearchToolConfig = field(default_factory=WebSearchToolConfig)
493
+ test_shell: TestShellToolConfig = field(default_factory=TestShellToolConfig)
474
494
 
475
495
  @classmethod
476
496
  def from_mapping(cls, raw: Mapping[str, Any]) -> Self:
477
- return cls(web_search=WebSearchToolConfig.from_mapping(_as_mapping(raw.get("web_search"))))
497
+ return cls(
498
+ web_search=WebSearchToolConfig.from_mapping(_as_mapping(raw.get("web_search"))),
499
+ test_shell=TestShellToolConfig.from_mapping(_as_mapping(raw.get("test_shell"))),
500
+ )
478
501
 
479
502
 
480
503
  @dataclass(frozen=True)
@@ -16,7 +16,8 @@ Supported operation types:
16
16
 
17
17
  - `create_file`: create a new text file with `file_path` and `content`.
18
18
  - `replace_file`: explicitly replace a whole existing file with `file_path`,
19
- `content`, `overwrite=true`, and either `snapshot_id` or `expected_hash`.
19
+ `content`, `overwrite=true`, and either `snapshot_token`, `snapshot_id`, or
20
+ `expected_hash`.
20
21
  - `delete_file`: delete `file_path`.
21
22
  - `move_file`: move `file_path` to `destination_path`.
22
23
  - `replace_block`: replace exact `old_text` with `new_text`.
@@ -41,6 +42,7 @@ Example:
41
42
  "replace_all": null,
42
43
  "overwrite": null,
43
44
  "snapshot_id": null,
45
+ "snapshot_token": null,
44
46
  "expected_hash": null
45
47
  },
46
48
  {
@@ -55,6 +57,7 @@ Example:
55
57
  "replace_all": null,
56
58
  "overwrite": null,
57
59
  "snapshot_id": null,
60
+ "snapshot_token": null,
58
61
  "expected_hash": null
59
62
  }
60
63
  ]
@@ -0,0 +1,19 @@
1
+ ## read_file
2
+
3
+ Read files or list directories before changes.
4
+
5
+ Args: `file_path`, optional `offset`, optional `limit`, optional `pages`.
6
+
7
+ Text output includes line numbers. Full text reads record a managed snapshot with
8
+ encoding, line-ending, `snapshot_id`, numeric `snapshot_token`, and content hash
9
+ metadata for later `edit_text`, `write_file`, or `apply_patch` calls. Prefer
10
+ `snapshot_token` for existing-file replacement when available because it avoids
11
+ identifier quoting mistakes; `snapshot_id` and content hash remain valid.
12
+ Partial reads return snippet metadata that can scope later `edit_text` calls but
13
+ do not authorize unrestricted whole-file replacement. For normal single-file
14
+ exact edits after a partial read, prefer `edit_text` with `file_path` and no
15
+ `snippet_id`; use the snippet only when you need to constrain the replacement to
16
+ that line range.
17
+
18
+ Non-text files such as images, notebooks, and PDFs may return descriptive
19
+ metadata, but they are not tracked for text mutation.
@@ -0,0 +1,16 @@
1
+ ## test_shell
2
+
3
+ Run constrained verification commands for tester subagents.
4
+
5
+ Args: `command`, optional `description`, optional `timeout_ms`, optional
6
+ `approval_token`.
7
+
8
+ `test_shell` parses the command into argv and does not run it through an
9
+ unrestricted raw shell. It rejects shell composition such as pipes, separators,
10
+ redirection, command substitution, heredocs, and background operators.
11
+
12
+ Low-risk verification commands run immediately and return command, cwd,
13
+ exit-code, elapsed time, stdout, stderr, and truncation metadata. Medium-risk
14
+ commands return `approval_required` with an `approvalToken`; the main Deepy
15
+ agent must ask the user before retrying the same command with that token.
16
+ Destructive, publishing, mutating, or unsupported commands are denied.
@@ -3,11 +3,12 @@
3
3
  Create a new text file or explicitly replace a whole file.
4
4
 
5
5
  Args: `file_path`, `content`, `overwrite`, optional `snapshot_id`, optional
6
- `expected_hash`.
6
+ `snapshot_token`, optional `expected_hash`.
7
7
 
8
8
  For new files, Deepy writes UTF-8 without BOM by default. For existing files,
9
9
  whole-file replacement requires `overwrite=true` and a fresh `snapshot_id` or
10
- `expected_hash` from `read_file`; this prevents accidental stale rewrites.
10
+ `snapshot_token` or `expected_hash` from `read_file`; this prevents accidental
11
+ stale rewrites. Prefer `snapshot_token` when available.
11
12
 
12
13
  Prefer `edit_text` for small targeted edits and `apply_patch` for structured or
13
14
  multi-file edits.
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from deepy.config import Settings
7
+ from deepy.mcp import sdk_mcp_tool_name
8
+ from deepy.prompts import build_system_prompt
9
+ from deepy.skills import SkillInfo
10
+ from deepy.subagents import SubagentDefinition, discover_subagents
11
+ from deepy.tools import ToolRuntime
12
+ from deepy.tools.agents import build_function_tools
13
+ from deepy.tools.result import ToolResult
14
+
15
+ from .provider import ProviderBundle, build_provider_bundle
16
+
17
+ if TYPE_CHECKING:
18
+ from agents.mcp import MCPServer
19
+
20
+
21
+ def build_deepy_agent(
22
+ settings: Settings,
23
+ runtime: ToolRuntime,
24
+ *,
25
+ project_root: Path,
26
+ provider: ProviderBundle | None = None,
27
+ loaded_skills: list[SkillInfo] | None = None,
28
+ mcp_servers: list[MCPServer] | None = None,
29
+ preferred_mcp_web_search_tools: list[str] | None = None,
30
+ emit_event: Any | None = None,
31
+ ):
32
+ from agents import Agent
33
+
34
+ provider = provider or build_provider_bundle(settings)
35
+ main_tools = build_function_tools(
36
+ runtime,
37
+ mimo_schema_compatibility=uses_mimo_tool_schema_compatibility(
38
+ settings.model.provider,
39
+ settings.model.name,
40
+ ),
41
+ preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
42
+ )
43
+ subagent_tools = build_subagent_tools(
44
+ settings,
45
+ runtime,
46
+ project_root=project_root,
47
+ provider=provider,
48
+ mcp_servers=list(mcp_servers or []),
49
+ preferred_mcp_web_search_tools=preferred_mcp_web_search_tools or [],
50
+ mimo_schema_compatibility=uses_mimo_tool_schema_compatibility(
51
+ settings.model.provider,
52
+ settings.model.name,
53
+ ),
54
+ emit_event=emit_event,
55
+ )
56
+ return Agent(
57
+ name="Deepy",
58
+ instructions=build_system_prompt(
59
+ project_root,
60
+ settings,
61
+ loaded_skills=loaded_skills,
62
+ preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
63
+ ),
64
+ model=provider.model,
65
+ model_settings=provider.model_settings,
66
+ tools=[*main_tools, *subagent_tools],
67
+ mcp_servers=list(mcp_servers or []),
68
+ mcp_config={"include_server_in_tool_names": True},
69
+ )
70
+
71
+
72
+ def uses_mimo_tool_schema_compatibility(provider: str, model: str) -> bool:
73
+ normalized_provider = provider.strip().lower()
74
+ normalized_model = model.strip().lower()
75
+ if normalized_provider == "xiaomi":
76
+ return normalized_model in {"mimo-v2.5", "mimo-v2.5-pro"}
77
+ if normalized_provider == "openrouter":
78
+ return normalized_model in {"xiaomi/mimo-v2.5", "xiaomi/mimo-v2.5-pro"}
79
+ return False
80
+
81
+
82
+ def build_subagent_tools(
83
+ settings: Settings,
84
+ runtime: ToolRuntime,
85
+ *,
86
+ project_root: Path,
87
+ provider: ProviderBundle,
88
+ mcp_servers: list[MCPServer],
89
+ preferred_mcp_web_search_tools: list[str],
90
+ mimo_schema_compatibility: bool = False,
91
+ emit_event: Any | None = None,
92
+ ) -> list[Any]:
93
+ from agents import Agent
94
+
95
+ discovery = discover_subagents(project_root)
96
+ tools: list[Any] = []
97
+ for definition in discovery.definitions:
98
+ subagent = Agent(
99
+ name=f"Deepy {definition.name}",
100
+ instructions=_subagent_instructions(definition, preferred_mcp_web_search_tools),
101
+ model=definition.model or provider.model,
102
+ model_settings=provider.model_settings,
103
+ tools=build_function_tools(
104
+ runtime,
105
+ mimo_schema_compatibility=mimo_schema_compatibility,
106
+ preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
107
+ include_tools=set(definition.tools),
108
+ ),
109
+ mcp_servers=_search_mcp_servers_for_subagent(
110
+ definition,
111
+ mcp_servers,
112
+ preferred_mcp_web_search_tools,
113
+ ),
114
+ mcp_config={"include_server_in_tool_names": True},
115
+ )
116
+ tools.append(
117
+ subagent.as_tool(
118
+ tool_name=definition.tool_name,
119
+ tool_description=definition.description,
120
+ custom_output_extractor=_subagent_output_extractor(definition),
121
+ on_stream=_subagent_stream_handler(definition, emit_event),
122
+ max_turns=definition.max_turns,
123
+ )
124
+ )
125
+ return tools
126
+
127
+
128
+ def _subagent_instructions(
129
+ definition: SubagentDefinition,
130
+ preferred_mcp_web_search_tools: list[str],
131
+ ) -> str:
132
+ search_mcp = ""
133
+ if definition.mcp.inherit_search and preferred_mcp_web_search_tools:
134
+ search_mcp = (
135
+ "\n\nSearch-class MCP tools available to this subagent: "
136
+ + ", ".join(preferred_mcp_web_search_tools)
137
+ + ". Use them only for search/current-information work."
138
+ )
139
+ return (
140
+ f"{definition.instructions.strip()}\n\n"
141
+ "Return one concise final report to the main Deepy agent. Include assigned scope, "
142
+ "key findings or actions, relevant file paths or commands, and unresolved issues. "
143
+ "Do not ask the user directly; report blockers or approval needs to the main agent."
144
+ f"{search_mcp}"
145
+ )
146
+
147
+
148
+ def _subagent_output_extractor(definition: SubagentDefinition):
149
+ async def extract(result: Any) -> str:
150
+ output = getattr(result, "final_output", "")
151
+ text = output if isinstance(output, str) else str(output or "")
152
+ return ToolResult.ok_result(
153
+ definition.tool_name,
154
+ text,
155
+ metadata={
156
+ "kind": "subagent_result",
157
+ "subagent": definition.name,
158
+ "source": definition.source,
159
+ },
160
+ ).to_json()
161
+
162
+ return extract
163
+
164
+
165
+ def _subagent_stream_handler(definition: SubagentDefinition, emit_event: Any | None):
166
+ async def handle(event: Any) -> None:
167
+ if emit_event is None:
168
+ return
169
+ from .events import DeepyStreamEvent, normalize_stream_event
170
+ from deepy.ui.message_view import format_tool_display_label
171
+
172
+ normalized = normalize_stream_event(event)
173
+ if normalized is None or normalized.kind != "tool_call":
174
+ return
175
+ nested_tool = normalized.name or "tool"
176
+ emit_event(
177
+ DeepyStreamEvent(
178
+ kind="status",
179
+ name=definition.tool_name,
180
+ text=(
181
+ f"{format_tool_display_label(definition.tool_name)} progress - "
182
+ f"using {nested_tool}"
183
+ ),
184
+ payload={
185
+ "kind": "subagent_progress",
186
+ "subagent": definition.name,
187
+ "tool": nested_tool,
188
+ },
189
+ )
190
+ )
191
+
192
+ return handle
193
+
194
+
195
+ def _search_mcp_servers_for_subagent(
196
+ definition: SubagentDefinition,
197
+ servers: list[MCPServer],
198
+ preferred_tools: list[str],
199
+ ) -> list[MCPServer]:
200
+ if not definition.mcp.inherit_search or not preferred_tools:
201
+ return []
202
+ allowed = set(preferred_tools)
203
+ filtered: list[MCPServer] = []
204
+ for server in servers:
205
+ if not _looks_like_mcp_server(server):
206
+ continue
207
+ filtered.append(_SearchOnlyMcpServer(server, allowed)) # type: ignore[arg-type]
208
+ return filtered
209
+
210
+
211
+ def _looks_like_mcp_server(server: object) -> bool:
212
+ return all(hasattr(server, attr) for attr in ("call_tool", "list_tools", "name"))
213
+
214
+
215
+ def _mcp_server_base() -> type[Any]:
216
+ from agents.mcp import MCPServer
217
+
218
+ return MCPServer
219
+
220
+
221
+ class _SearchOnlyMcpServer(_mcp_server_base()):
222
+ def __init__(self, wrapped: MCPServer, allowed_model_names: set[str]) -> None:
223
+ super().__init__(
224
+ use_structured_content=bool(getattr(wrapped, "use_structured_content", False)),
225
+ failure_error_function=getattr(wrapped, "_failure_error_function", None),
226
+ )
227
+ self._wrapped = wrapped
228
+ self._allowed_model_names = allowed_model_names
229
+
230
+ @property
231
+ def name(self) -> str:
232
+ return str(getattr(self._wrapped, "name", "mcp"))
233
+
234
+ @property
235
+ def cached_tools(self) -> Any:
236
+ return getattr(self._wrapped, "cached_tools", None)
237
+
238
+ async def connect(self) -> None:
239
+ return None
240
+
241
+ async def cleanup(self) -> None:
242
+ return None
243
+
244
+ async def list_tools(self, *args: Any, **kwargs: Any) -> list[Any]:
245
+ listed = await self._wrapped.list_tools(*args, **kwargs)
246
+ return [
247
+ tool
248
+ for tool in listed
249
+ if sdk_mcp_tool_name(self.name, str(getattr(tool, "name", "")))
250
+ in self._allowed_model_names
251
+ ]
252
+
253
+ async def call_tool(
254
+ self,
255
+ tool_name: str,
256
+ arguments: dict[str, Any] | None,
257
+ meta: dict[str, Any] | None = None,
258
+ ) -> Any:
259
+ if sdk_mcp_tool_name(self.name, tool_name) not in self._allowed_model_names:
260
+ raise PermissionError(f"MCP tool is not available to this subagent: {tool_name}")
261
+ return await self._wrapped.call_tool(tool_name, arguments, meta)
262
+
263
+ async def list_prompts(self) -> Any:
264
+ return await self._wrapped.list_prompts()
265
+
266
+ async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
267
+ return await self._wrapped.get_prompt(name, arguments)
@@ -93,6 +93,7 @@ async def run_prompt_once(
93
93
  loaded_skills=loaded_skills,
94
94
  mcp_servers=mcp_runtime.active_servers,
95
95
  preferred_mcp_web_search_tools=mcp_runtime.preferred_web_search_tools,
96
+ emit_event=emit_event,
96
97
  )
97
98
  started_at = time.time()
98
99
  try:
@@ -48,6 +48,7 @@ Core rules:
48
48
  - Use `Search` for local project code/text search instead of shell `grep`, `find`, or `rg`; narrow with `path`, `glob`, `output_mode`, `limit`, and `offset`.
49
49
  - Read existing files when you need context; exact `edit_text` edits can establish the managed snapshot internally.
50
50
  - Use `edit_text` for one small single-file exact edit. Use structured `apply_patch.operations` when a change has multiple edits in one file, touches multiple files, creates/deletes/moves files, or replaces a larger block. Use `write_file` for new files or explicit whole-file replacement.
51
+ - For existing-file replacement, pass `overwrite=true` plus the fresh `snapshot_token` from `read_file` when available; `snapshot_id` and content hash are also valid freshness tokens.
51
52
  - After project generators create scaffold files, read and edit the generated block instead of replacing the file.
52
53
  - Run shell commands using the Runtime context's command dialect and path style: `powershell` -> PowerShell with Windows paths; `cmd` -> cmd; `posix` -> POSIX shell.
53
54
  - Match visible thinking/reasoning language to the user's latest natural language. If the user asks in Chinese, you MUST write visible thinking/reasoning in Chinese unless they explicitly request another language. Do not switch visible thinking/reasoning to English for Chinese requests.
@@ -61,6 +62,14 @@ Core rules:
61
62
  tasks so progress tracking does not create noise.
62
63
  - `todo_write` is only for local task tracking. Do not treat it as subagent
63
64
  delegation, a `task` tool, or a plan approval mode.
65
+ - Use subagent tools when a task has a clear independent specialist slice:
66
+ `subagent_explore` for broad read-only investigation, `subagent_reviewer` for
67
+ focused review, and `subagent_tester` for reproduction or verification. Keep
68
+ Deepy responsible for final synthesis and do not delegate tiny one-step work.
69
+ - If a subagent reports `test_shell` `approval_required`, ask the user through
70
+ `AskUserQuestion` with the exact command, policy reason, and approval token.
71
+ Retry only the same command through the constrained `test_shell` path after
72
+ the user approves; do not broaden access to raw shell.
64
73
 
65
74
  Tool protocol:
66
75
  Tool results are JSON strings: ok, name, output, error, metadata, awaitUserResponse.
@@ -8,6 +8,7 @@ TOOL_DOC_FILES = (
8
8
  "task_list.md",
9
9
  "task_output.md",
10
10
  "task_stop.md",
11
+ "test_shell.md",
11
12
  "Search.md",
12
13
  "read_file.md",
13
14
  "edit_text.md",