deepy-cli 0.2.14__tar.gz → 0.2.16__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.14 → deepy_cli-0.2.16}/PKG-INFO +9 -1
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/README.md +8 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/pyproject.toml +1 -1
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/__init__.py +1 -1
- deepy_cli-0.2.16/src/deepy/background_tasks.py +359 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/config/settings.py +6 -4
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/shell.md +7 -1
- deepy_cli-0.2.16/src/deepy/data/tools/task_list.md +11 -0
- deepy_cli-0.2.16/src/deepy/data/tools/task_output.md +11 -0
- deepy_cli-0.2.16/src/deepy/data/tools/task_stop.md +9 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/agent.py +14 -0
- deepy_cli-0.2.16/src/deepy/llm/provider.py +143 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/runner.py +4 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/tool_docs.py +3 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/agents.py +189 -20
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/builtin.py +299 -7
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/app.py +149 -3
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/commands.py +2 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/message_view.py +25 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/slash_commands.py +2 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/status_footer.py +1 -1
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/terminal.py +192 -2
- deepy_cli-0.2.14/src/deepy/llm/provider.py +0 -82
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/Search.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/apply_patch.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/edit_text.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/read_file.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/todo_write.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/write_file.md +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/input_suggestions.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/session_cost.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/todos.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/search.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/compat.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/diff.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/runner.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/screens.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/state.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/widgets.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/prompt_input.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.14 → deepy_cli-0.2.16}/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.
|
|
3
|
+
Version: 0.2.16
|
|
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
|
|
@@ -254,6 +254,8 @@ Inside an interactive Deepy session:
|
|
|
254
254
|
```text
|
|
255
255
|
/model Select provider, model, and thinking mode
|
|
256
256
|
/status Show usage, context pressure, and DeepSeek balance
|
|
257
|
+
/ps Show managed background shell tasks
|
|
258
|
+
/stop Choose background shell tasks to stop
|
|
257
259
|
/resume Resume a previous project session
|
|
258
260
|
/new Start a fresh session
|
|
259
261
|
/compact Compact the active session context
|
|
@@ -270,6 +272,10 @@ Typical usage:
|
|
|
270
272
|
Ask Deepy to inspect a bug, edit files, run tests, and summarize what changed.
|
|
271
273
|
Use @ to reference files precisely.
|
|
272
274
|
Use ! for commands you want to run directly without model mediation.
|
|
275
|
+
For long-running servers or watchers, Deepy can run shell commands as managed
|
|
276
|
+
background tasks. Their output is captured separately; use `/ps`, `/stop` to
|
|
277
|
+
choose one task or all tasks, or use the model-facing `task_output` tool to
|
|
278
|
+
inspect and manage them.
|
|
273
279
|
Use /resume when returning to a project later.
|
|
274
280
|
Use /compact when a long session needs a durable summary.
|
|
275
281
|
```
|
|
@@ -376,6 +382,8 @@ Inside the interactive terminal:
|
|
|
376
382
|
/skill:<name> [request] Invoke a skill directly
|
|
377
383
|
/init Create or update project AGENTS.md
|
|
378
384
|
/mcp Show MCP server status and tools
|
|
385
|
+
/ps Show managed background shell tasks
|
|
386
|
+
/stop Choose background shell tasks to stop
|
|
379
387
|
/status Show usage, context pressure, and DeepSeek balance
|
|
380
388
|
```
|
|
381
389
|
|
|
@@ -222,6 +222,8 @@ Inside an interactive Deepy session:
|
|
|
222
222
|
```text
|
|
223
223
|
/model Select provider, model, and thinking mode
|
|
224
224
|
/status Show usage, context pressure, and DeepSeek balance
|
|
225
|
+
/ps Show managed background shell tasks
|
|
226
|
+
/stop Choose background shell tasks to stop
|
|
225
227
|
/resume Resume a previous project session
|
|
226
228
|
/new Start a fresh session
|
|
227
229
|
/compact Compact the active session context
|
|
@@ -238,6 +240,10 @@ Typical usage:
|
|
|
238
240
|
Ask Deepy to inspect a bug, edit files, run tests, and summarize what changed.
|
|
239
241
|
Use @ to reference files precisely.
|
|
240
242
|
Use ! for commands you want to run directly without model mediation.
|
|
243
|
+
For long-running servers or watchers, Deepy can run shell commands as managed
|
|
244
|
+
background tasks. Their output is captured separately; use `/ps`, `/stop` to
|
|
245
|
+
choose one task or all tasks, or use the model-facing `task_output` tool to
|
|
246
|
+
inspect and manage them.
|
|
241
247
|
Use /resume when returning to a project later.
|
|
242
248
|
Use /compact when a long session needs a durable summary.
|
|
243
249
|
```
|
|
@@ -344,6 +350,8 @@ Inside the interactive terminal:
|
|
|
344
350
|
/skill:<name> [request] Invoke a skill directly
|
|
345
351
|
/init Create or update project AGENTS.md
|
|
346
352
|
/mcp Show MCP server status and tools
|
|
353
|
+
/ps Show managed background shell tasks
|
|
354
|
+
/stop Choose background shell tasks to stop
|
|
347
355
|
/status Show usage, context pressure, and DeepSeek balance
|
|
348
356
|
```
|
|
349
357
|
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
BackgroundTaskStatus = Literal["running", "completed", "failed", "stopped"]
|
|
18
|
+
DEFAULT_MAX_RUNNING_TASKS = 4
|
|
19
|
+
DEFAULT_MAX_TERMINAL_TASKS = 32
|
|
20
|
+
DEFAULT_OUTPUT_PREVIEW_BYTES = 32 * 1024
|
|
21
|
+
DEFAULT_STOP_GRACE_SECONDS = 2.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class BackgroundTaskSnapshot:
|
|
26
|
+
id: str
|
|
27
|
+
command: str
|
|
28
|
+
cwd: str
|
|
29
|
+
status: BackgroundTaskStatus
|
|
30
|
+
start_time: float
|
|
31
|
+
output_path: Path
|
|
32
|
+
pid: int | None = None
|
|
33
|
+
end_time: float | None = None
|
|
34
|
+
exit_code: int | None = None
|
|
35
|
+
error: str | None = None
|
|
36
|
+
stop_requested: bool = False
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def running(self) -> bool:
|
|
40
|
+
return self.status == "running"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BackgroundTaskOutput:
|
|
45
|
+
task: BackgroundTaskSnapshot
|
|
46
|
+
output: str
|
|
47
|
+
output_size_bytes: int
|
|
48
|
+
output_preview_bytes: int
|
|
49
|
+
output_truncated: bool
|
|
50
|
+
more_available: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class BackgroundTaskStopSummary:
|
|
55
|
+
stopped: tuple[BackgroundTaskSnapshot, ...]
|
|
56
|
+
not_found: tuple[str, ...] = ()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class _BackgroundTaskEntry:
|
|
61
|
+
id: str
|
|
62
|
+
command: str
|
|
63
|
+
cwd: Path
|
|
64
|
+
start_time: float
|
|
65
|
+
output_path: Path
|
|
66
|
+
process: subprocess.Popen[bytes] | None
|
|
67
|
+
status: BackgroundTaskStatus = "running"
|
|
68
|
+
pid: int | None = None
|
|
69
|
+
end_time: float | None = None
|
|
70
|
+
exit_code: int | None = None
|
|
71
|
+
error: str | None = None
|
|
72
|
+
stop_requested: bool = False
|
|
73
|
+
lock: threading.RLock = field(default_factory=threading.RLock)
|
|
74
|
+
|
|
75
|
+
def snapshot(self) -> BackgroundTaskSnapshot:
|
|
76
|
+
with self.lock:
|
|
77
|
+
return BackgroundTaskSnapshot(
|
|
78
|
+
id=self.id,
|
|
79
|
+
command=self.command,
|
|
80
|
+
cwd=str(self.cwd),
|
|
81
|
+
status=self.status,
|
|
82
|
+
start_time=self.start_time,
|
|
83
|
+
end_time=self.end_time,
|
|
84
|
+
output_path=self.output_path,
|
|
85
|
+
pid=self.pid,
|
|
86
|
+
exit_code=self.exit_code,
|
|
87
|
+
error=self.error,
|
|
88
|
+
stop_requested=self.stop_requested,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BackgroundTaskError(RuntimeError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BackgroundTaskLimitError(BackgroundTaskError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BackgroundTaskManager:
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
base_dir: Path | None = None,
|
|
105
|
+
max_running_tasks: int = DEFAULT_MAX_RUNNING_TASKS,
|
|
106
|
+
max_terminal_tasks: int = DEFAULT_MAX_TERMINAL_TASKS,
|
|
107
|
+
stop_grace_seconds: float = DEFAULT_STOP_GRACE_SECONDS,
|
|
108
|
+
) -> None:
|
|
109
|
+
root = base_dir or Path(tempfile.gettempdir()) / "deepy-background-tasks" / uuid.uuid4().hex
|
|
110
|
+
self.base_dir = root
|
|
111
|
+
self.max_running_tasks = max(1, max_running_tasks)
|
|
112
|
+
self.max_terminal_tasks = max(0, max_terminal_tasks)
|
|
113
|
+
self.stop_grace_seconds = max(0.0, stop_grace_seconds)
|
|
114
|
+
self._entries: dict[str, _BackgroundTaskEntry] = {}
|
|
115
|
+
self._lock = threading.RLock()
|
|
116
|
+
self._next_id = 1
|
|
117
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
def start(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
command: str,
|
|
123
|
+
argv: Sequence[str],
|
|
124
|
+
cwd: Path,
|
|
125
|
+
env: dict[str, str] | None = None,
|
|
126
|
+
) -> BackgroundTaskSnapshot:
|
|
127
|
+
with self._lock:
|
|
128
|
+
running = sum(1 for entry in self._entries.values() if entry.status == "running")
|
|
129
|
+
if running >= self.max_running_tasks:
|
|
130
|
+
raise BackgroundTaskLimitError(
|
|
131
|
+
f"Background task limit reached ({self.max_running_tasks} running)."
|
|
132
|
+
)
|
|
133
|
+
task_id = self._new_task_id()
|
|
134
|
+
output_path = self.base_dir / f"{task_id}.log"
|
|
135
|
+
|
|
136
|
+
output_file = output_path.open("ab")
|
|
137
|
+
entry: _BackgroundTaskEntry | None = None
|
|
138
|
+
try:
|
|
139
|
+
process = subprocess.Popen(
|
|
140
|
+
argv,
|
|
141
|
+
cwd=cwd,
|
|
142
|
+
env=env,
|
|
143
|
+
stdin=subprocess.DEVNULL,
|
|
144
|
+
stdout=output_file,
|
|
145
|
+
stderr=subprocess.STDOUT,
|
|
146
|
+
start_new_session=os.name != "nt",
|
|
147
|
+
)
|
|
148
|
+
output_file.close()
|
|
149
|
+
entry = _BackgroundTaskEntry(
|
|
150
|
+
id=task_id,
|
|
151
|
+
command=command,
|
|
152
|
+
cwd=cwd,
|
|
153
|
+
start_time=time.time(),
|
|
154
|
+
output_path=output_path,
|
|
155
|
+
process=process,
|
|
156
|
+
pid=process.pid,
|
|
157
|
+
)
|
|
158
|
+
with self._lock:
|
|
159
|
+
self._entries[task_id] = entry
|
|
160
|
+
thread = threading.Thread(target=self._wait_for_process, args=(entry,), daemon=True)
|
|
161
|
+
thread.start()
|
|
162
|
+
return entry.snapshot()
|
|
163
|
+
except Exception:
|
|
164
|
+
with contextlib.suppress(Exception):
|
|
165
|
+
output_file.close()
|
|
166
|
+
if entry is not None:
|
|
167
|
+
with self._lock:
|
|
168
|
+
self._entries.pop(entry.id, None)
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
def get(self, task_id: str) -> BackgroundTaskSnapshot | None:
|
|
172
|
+
with self._lock:
|
|
173
|
+
entry = self._entries.get(task_id)
|
|
174
|
+
return entry.snapshot() if entry is not None else None
|
|
175
|
+
|
|
176
|
+
def list(self, *, active_only: bool = False, limit: int | None = None) -> Sequence[BackgroundTaskSnapshot]:
|
|
177
|
+
with self._lock:
|
|
178
|
+
snapshots = [entry.snapshot() for entry in self._entries.values()]
|
|
179
|
+
if active_only:
|
|
180
|
+
snapshots = [snapshot for snapshot in snapshots if snapshot.status == "running"]
|
|
181
|
+
snapshots.sort(
|
|
182
|
+
key=lambda item: (
|
|
183
|
+
0 if item.status == "running" else 1,
|
|
184
|
+
-(item.start_time if item.status == "running" else item.end_time or item.start_time),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
return snapshots[:limit] if limit is not None else snapshots
|
|
188
|
+
|
|
189
|
+
def has_running(self) -> bool:
|
|
190
|
+
with self._lock:
|
|
191
|
+
return any(entry.status == "running" for entry in self._entries.values())
|
|
192
|
+
|
|
193
|
+
def running_count(self) -> int:
|
|
194
|
+
with self._lock:
|
|
195
|
+
return sum(1 for entry in self._entries.values() if entry.status == "running")
|
|
196
|
+
|
|
197
|
+
def read_output(
|
|
198
|
+
self,
|
|
199
|
+
task_id: str,
|
|
200
|
+
*,
|
|
201
|
+
max_bytes: int = DEFAULT_OUTPUT_PREVIEW_BYTES,
|
|
202
|
+
) -> BackgroundTaskOutput | None:
|
|
203
|
+
snapshot = self.get(task_id)
|
|
204
|
+
if snapshot is None:
|
|
205
|
+
return None
|
|
206
|
+
max_bytes = max(1, max_bytes)
|
|
207
|
+
try:
|
|
208
|
+
output_size = snapshot.output_path.stat().st_size if snapshot.output_path.exists() else 0
|
|
209
|
+
offset = max(0, output_size - max_bytes)
|
|
210
|
+
with snapshot.output_path.open("rb") as file:
|
|
211
|
+
file.seek(offset)
|
|
212
|
+
data = file.read(max_bytes)
|
|
213
|
+
except OSError:
|
|
214
|
+
output_size = 0
|
|
215
|
+
data = b""
|
|
216
|
+
offset = 0
|
|
217
|
+
return BackgroundTaskOutput(
|
|
218
|
+
task=snapshot,
|
|
219
|
+
output=data.decode("utf-8", errors="replace"),
|
|
220
|
+
output_size_bytes=output_size,
|
|
221
|
+
output_preview_bytes=len(data),
|
|
222
|
+
output_truncated=offset > 0,
|
|
223
|
+
more_available=offset > 0,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def wait(self, task_id: str, *, timeout_seconds: float) -> BackgroundTaskSnapshot | None:
|
|
227
|
+
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
|
228
|
+
while True:
|
|
229
|
+
snapshot = self.get(task_id)
|
|
230
|
+
if snapshot is None or snapshot.status != "running":
|
|
231
|
+
return snapshot
|
|
232
|
+
if time.monotonic() >= deadline:
|
|
233
|
+
return snapshot
|
|
234
|
+
time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
|
|
235
|
+
|
|
236
|
+
def wait_for_output(self, task_id: str, *, timeout_seconds: float) -> BackgroundTaskSnapshot | None:
|
|
237
|
+
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
|
238
|
+
while True:
|
|
239
|
+
snapshot = self.get(task_id)
|
|
240
|
+
if snapshot is None or snapshot.status != "running":
|
|
241
|
+
return snapshot
|
|
242
|
+
with contextlib.suppress(OSError):
|
|
243
|
+
if snapshot.output_path.exists() and snapshot.output_path.stat().st_size > 0:
|
|
244
|
+
return snapshot
|
|
245
|
+
if time.monotonic() >= deadline:
|
|
246
|
+
return snapshot
|
|
247
|
+
time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
|
|
248
|
+
|
|
249
|
+
def stop(self, task_id: str, *, force_after_grace: bool = False) -> BackgroundTaskSnapshot | None:
|
|
250
|
+
with self._lock:
|
|
251
|
+
entry = self._entries.get(task_id)
|
|
252
|
+
if entry is None:
|
|
253
|
+
return None
|
|
254
|
+
self._request_stop(entry)
|
|
255
|
+
if force_after_grace:
|
|
256
|
+
self._wait_or_force(entry, self.stop_grace_seconds)
|
|
257
|
+
return entry.snapshot()
|
|
258
|
+
|
|
259
|
+
def stop_all(self, *, force_after_grace: bool = True) -> BackgroundTaskStopSummary:
|
|
260
|
+
with self._lock:
|
|
261
|
+
entries = [entry for entry in self._entries.values() if entry.status == "running"]
|
|
262
|
+
for entry in entries:
|
|
263
|
+
self._request_stop(entry)
|
|
264
|
+
if force_after_grace:
|
|
265
|
+
for entry in entries:
|
|
266
|
+
self._wait_or_force(entry, self.stop_grace_seconds)
|
|
267
|
+
return BackgroundTaskStopSummary(stopped=tuple(entry.snapshot() for entry in entries))
|
|
268
|
+
|
|
269
|
+
def _new_task_id(self) -> str:
|
|
270
|
+
task_id = f"bg-{self._next_id}"
|
|
271
|
+
self._next_id += 1
|
|
272
|
+
return task_id
|
|
273
|
+
|
|
274
|
+
def _wait_for_process(self, entry: _BackgroundTaskEntry) -> None:
|
|
275
|
+
process = entry.process
|
|
276
|
+
if process is None:
|
|
277
|
+
return
|
|
278
|
+
try:
|
|
279
|
+
returncode = process.wait()
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
self._settle(entry, "failed", error=str(exc))
|
|
282
|
+
return
|
|
283
|
+
if entry.snapshot().stop_requested:
|
|
284
|
+
self._settle(entry, "stopped", exit_code=returncode)
|
|
285
|
+
elif returncode == 0:
|
|
286
|
+
self._settle(entry, "completed", exit_code=returncode)
|
|
287
|
+
else:
|
|
288
|
+
self._settle(entry, "failed", exit_code=returncode, error=f"Command exited with code {returncode}.")
|
|
289
|
+
|
|
290
|
+
def _settle(
|
|
291
|
+
self,
|
|
292
|
+
entry: _BackgroundTaskEntry,
|
|
293
|
+
status: BackgroundTaskStatus,
|
|
294
|
+
*,
|
|
295
|
+
exit_code: int | None = None,
|
|
296
|
+
error: str | None = None,
|
|
297
|
+
) -> None:
|
|
298
|
+
with entry.lock:
|
|
299
|
+
if entry.status != "running":
|
|
300
|
+
return
|
|
301
|
+
entry.status = status
|
|
302
|
+
entry.exit_code = exit_code
|
|
303
|
+
entry.error = error
|
|
304
|
+
entry.end_time = time.time()
|
|
305
|
+
self._prune_terminal_entries()
|
|
306
|
+
|
|
307
|
+
def _request_stop(self, entry: _BackgroundTaskEntry) -> None:
|
|
308
|
+
with entry.lock:
|
|
309
|
+
if entry.status != "running":
|
|
310
|
+
return
|
|
311
|
+
entry.stop_requested = True
|
|
312
|
+
process = entry.process
|
|
313
|
+
if process is None or process.poll() is not None:
|
|
314
|
+
return
|
|
315
|
+
_terminate_process(process)
|
|
316
|
+
|
|
317
|
+
def _wait_or_force(self, entry: _BackgroundTaskEntry, grace_seconds: float) -> None:
|
|
318
|
+
process = entry.process
|
|
319
|
+
if process is None:
|
|
320
|
+
return
|
|
321
|
+
try:
|
|
322
|
+
process.wait(timeout=grace_seconds)
|
|
323
|
+
except subprocess.TimeoutExpired:
|
|
324
|
+
_kill_process(process)
|
|
325
|
+
with contextlib.suppress(Exception):
|
|
326
|
+
process.wait(timeout=0.5)
|
|
327
|
+
snapshot = entry.snapshot()
|
|
328
|
+
if snapshot.status == "running" and snapshot.stop_requested and process.poll() is not None:
|
|
329
|
+
self._settle(entry, "stopped", exit_code=process.returncode)
|
|
330
|
+
|
|
331
|
+
def _prune_terminal_entries(self) -> None:
|
|
332
|
+
with self._lock:
|
|
333
|
+
terminal = [
|
|
334
|
+
entry
|
|
335
|
+
for entry in self._entries.values()
|
|
336
|
+
if entry.status != "running"
|
|
337
|
+
]
|
|
338
|
+
terminal.sort(key=lambda entry: (entry.end_time or entry.start_time, entry.start_time))
|
|
339
|
+
while len(terminal) > self.max_terminal_tasks:
|
|
340
|
+
oldest = terminal.pop(0)
|
|
341
|
+
self._entries.pop(oldest.id, None)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _terminate_process(process: subprocess.Popen[bytes]) -> None:
|
|
345
|
+
if os.name == "nt":
|
|
346
|
+
with contextlib.suppress(OSError):
|
|
347
|
+
process.terminate()
|
|
348
|
+
return
|
|
349
|
+
with contextlib.suppress(OSError, ProcessLookupError):
|
|
350
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _kill_process(process: subprocess.Popen[bytes]) -> None:
|
|
354
|
+
if os.name == "nt":
|
|
355
|
+
with contextlib.suppress(OSError):
|
|
356
|
+
process.kill()
|
|
357
|
+
return
|
|
358
|
+
with contextlib.suppress(OSError, ProcessLookupError):
|
|
359
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
@@ -27,7 +27,7 @@ DEFAULT_PROVIDER = "deepseek"
|
|
|
27
27
|
DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
28
28
|
DEFAULT_XIAOMI_BASE_URL = "https://api.xiaomimimo.com/v1"
|
|
29
29
|
DEEPSEEK_REASONING_EFFORTS = {"high", "max"}
|
|
30
|
-
SWITCH_ONLY_REASONING_EFFORTS = {"
|
|
30
|
+
SWITCH_ONLY_REASONING_EFFORTS = {"enabled", "none"}
|
|
31
31
|
OPENROUTER_REASONING_MODES = (
|
|
32
32
|
"enabled",
|
|
33
33
|
"disabled",
|
|
@@ -245,9 +245,11 @@ def normalize_reasoning_effort(
|
|
|
245
245
|
return value
|
|
246
246
|
return provider_info.default_thinking_mode
|
|
247
247
|
if provider_info.thinking_modes == SWITCH_ONLY_THINKING_MODES:
|
|
248
|
-
if thinking is False or value
|
|
248
|
+
if thinking is False or value in {"none", "disabled"}:
|
|
249
249
|
return "none"
|
|
250
|
-
|
|
250
|
+
if thinking is True or value == "enabled":
|
|
251
|
+
return "enabled"
|
|
252
|
+
return provider_info.default_thinking_mode
|
|
251
253
|
if value in DEEPSEEK_REASONING_EFFORTS:
|
|
252
254
|
return value
|
|
253
255
|
return provider_info.default_thinking_mode
|
|
@@ -271,7 +273,7 @@ def reasoning_effort_for_mode(mode: str, provider: str) -> str:
|
|
|
271
273
|
return mode
|
|
272
274
|
return provider_info_for(provider).default_thinking_mode
|
|
273
275
|
if provider_info_for(provider).thinking_modes == SWITCH_ONLY_THINKING_MODES:
|
|
274
|
-
return "none" if mode == "disabled" else "
|
|
276
|
+
return "none" if mode == "disabled" else "enabled"
|
|
275
277
|
return mode if mode in DEEPSEEK_REASONING_EFFORTS else "max"
|
|
276
278
|
|
|
277
279
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Run commands in the detected runtime shell for inspection, tests, builds, and
|
|
4
4
|
project operations.
|
|
5
5
|
|
|
6
|
-
Args: `command`, optional `timeout_ms`.
|
|
6
|
+
Args: `command`, optional `timeout_ms`, optional `run_in_background`.
|
|
7
7
|
|
|
8
8
|
Use the runtime context's command dialect and path style: PowerShell uses
|
|
9
9
|
PowerShell commands and Windows paths, `cmd` uses cmd syntax, and `posix` uses
|
|
@@ -16,3 +16,9 @@ run `chcp` or change their PowerShell profile for Unicode output.
|
|
|
16
16
|
|
|
17
17
|
Runs in the session cwd, preserves cwd between calls when supported, and returns
|
|
18
18
|
stdout/stderr JSON with cwd, exit-code, and shell metadata.
|
|
19
|
+
|
|
20
|
+
Set `run_in_background` to `true` only for long-running servers, watchers, or
|
|
21
|
+
jobs that should continue while you respond. Background commands return a task
|
|
22
|
+
id immediately and write stdout/stderr to a Deepy-managed log; use `task_list`,
|
|
23
|
+
`task_output`, and `task_stop` to manage them. Do not use background mode for
|
|
24
|
+
ordinary short commands where the result is needed before responding.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## task_list
|
|
2
|
+
|
|
3
|
+
List Deepy-managed background shell tasks that were started with
|
|
4
|
+
`shell(run_in_background=true)`.
|
|
5
|
+
|
|
6
|
+
Args: optional `active_only`, optional `limit`.
|
|
7
|
+
|
|
8
|
+
Use this before inspecting or stopping a background task when you need the task
|
|
9
|
+
id or current status. The output includes task ids, status, pid when available,
|
|
10
|
+
exit code when finished, and the original command. It never streams task output
|
|
11
|
+
into normal assistant thinking or response text.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## task_output
|
|
2
|
+
|
|
3
|
+
Read captured stdout/stderr for a Deepy-managed background shell task.
|
|
4
|
+
|
|
5
|
+
Args: `task_id`, optional `block`, optional `timeout`.
|
|
6
|
+
|
|
7
|
+
Use `block=true` with a small timeout when startup output is expected. Deepy
|
|
8
|
+
waits only until output appears, the task exits, or the bounded timeout expires;
|
|
9
|
+
it does not wait for long-running servers or watchers to finish. The result
|
|
10
|
+
returns a bounded tail of the task log plus size metadata, and indicates when
|
|
11
|
+
earlier output is available but not included.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## task_stop
|
|
2
|
+
|
|
3
|
+
Request termination of a Deepy-managed background shell task.
|
|
4
|
+
|
|
5
|
+
Args: `task_id`.
|
|
6
|
+
|
|
7
|
+
Use this when a server, watcher, or long-running job is no longer needed. Deepy
|
|
8
|
+
tracks the stop request and updates the task status after the process exits.
|
|
9
|
+
When the user exits Deepy, remaining background tasks are stopped automatically.
|
|
@@ -40,8 +40,22 @@ def build_deepy_agent(
|
|
|
40
40
|
model_settings=provider.model_settings,
|
|
41
41
|
tools=build_function_tools(
|
|
42
42
|
runtime,
|
|
43
|
+
mimo_schema_compatibility=uses_mimo_tool_schema_compatibility(
|
|
44
|
+
settings.model.provider,
|
|
45
|
+
settings.model.name,
|
|
46
|
+
),
|
|
43
47
|
preferred_mcp_web_search_tools=preferred_mcp_web_search_tools,
|
|
44
48
|
),
|
|
45
49
|
mcp_servers=list(mcp_servers or []),
|
|
46
50
|
mcp_config={"include_server_in_tool_names": True},
|
|
47
51
|
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def uses_mimo_tool_schema_compatibility(provider: str, model: str) -> bool:
|
|
55
|
+
normalized_provider = provider.strip().lower()
|
|
56
|
+
normalized_model = model.strip().lower()
|
|
57
|
+
if normalized_provider == "xiaomi":
|
|
58
|
+
return normalized_model in {"mimo-v2.5", "mimo-v2.5-pro"}
|
|
59
|
+
if normalized_provider == "openrouter":
|
|
60
|
+
return normalized_model in {"xiaomi/mimo-v2.5", "xiaomi/mimo-v2.5-pro"}
|
|
61
|
+
return False
|