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.
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/PKG-INFO +1 -1
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/pyproject.toml +1 -1
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/config/__init__.py +4 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/config/settings.py +24 -1
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/apply_patch.md +4 -1
- deepy_cli-0.2.18/src/deepy/data/tools/read_file.md +19 -0
- deepy_cli-0.2.18/src/deepy/data/tools/test_shell.md +16 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/write_file.md +3 -2
- deepy_cli-0.2.18/src/deepy/llm/agent.py +267 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/runner.py +1 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/system.py +9 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/tool_docs.py +1 -0
- deepy_cli-0.2.18/src/deepy/subagents.py +323 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/agents.py +339 -36
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/builtin.py +84 -10
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/file_state.py +8 -0
- deepy_cli-0.2.18/src/deepy/tools/test_shell.py +529 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/app.py +73 -9
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/widgets.py +45 -4
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/message_view.py +87 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/slash_commands.py +48 -14
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/terminal.py +141 -45
- deepy_cli-0.2.16/src/deepy/data/tools/read_file.md +0 -16
- deepy_cli-0.2.16/src/deepy/llm/agent.py +0 -61
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/README.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/background_tasks.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/Search.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/edit_text.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_list.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_output.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/task_stop.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/data/tools/todo_write.md +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/input_suggestions.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/session_cost.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/todos.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/search.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/commands.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/compat.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/diff.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/runner.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/screens.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/tui/state.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/prompt_input.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/status_footer.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.16 → deepy_cli-0.2.18}/src/deepy/utils/notify.py +0 -0
|
@@ -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(
|
|
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
|
|
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
|
|
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)
|
|
@@ -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.
|