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.
Files changed (104) hide show
  1. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/PKG-INFO +9 -1
  2. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/README.md +8 -0
  3. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/pyproject.toml +1 -1
  4. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/__init__.py +1 -1
  5. deepy_cli-0.2.16/src/deepy/background_tasks.py +359 -0
  6. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/config/settings.py +6 -4
  7. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/shell.md +7 -1
  8. deepy_cli-0.2.16/src/deepy/data/tools/task_list.md +11 -0
  9. deepy_cli-0.2.16/src/deepy/data/tools/task_output.md +11 -0
  10. deepy_cli-0.2.16/src/deepy/data/tools/task_stop.md +9 -0
  11. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/agent.py +14 -0
  12. deepy_cli-0.2.16/src/deepy/llm/provider.py +143 -0
  13. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/runner.py +4 -0
  14. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/tool_docs.py +3 -0
  15. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/agents.py +189 -20
  16. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/builtin.py +299 -7
  17. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/app.py +149 -3
  18. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/commands.py +2 -0
  19. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/message_view.py +25 -0
  20. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/slash_commands.py +2 -0
  21. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/status_footer.py +1 -1
  22. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/terminal.py +192 -2
  23. deepy_cli-0.2.14/src/deepy/llm/provider.py +0 -82
  24. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/__main__.py +0 -0
  25. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/cli.py +0 -0
  26. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/config/__init__.py +0 -0
  27. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/__init__.py +0 -0
  28. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  29. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  30. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  31. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/Search.md +0 -0
  32. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/WebFetch.md +0 -0
  33. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/WebSearch.md +0 -0
  34. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/__init__.py +0 -0
  35. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/apply_patch.md +0 -0
  36. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/edit_text.md +0 -0
  37. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/read_file.md +0 -0
  38. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/todo_write.md +0 -0
  39. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/data/tools/write_file.md +0 -0
  40. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/errors.py +0 -0
  41. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/input_suggestions.py +0 -0
  42. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/__init__.py +0 -0
  43. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/compaction.py +0 -0
  44. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/context.py +0 -0
  45. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/events.py +0 -0
  46. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/model_capabilities.py +0 -0
  47. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/replay.py +0 -0
  48. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/llm/thinking.py +0 -0
  49. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/mcp.py +0 -0
  50. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/__init__.py +0 -0
  51. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/compact.py +0 -0
  52. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/init_agents.py +0 -0
  53. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/rules.py +0 -0
  54. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/runtime_context.py +0 -0
  55. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/prompts/system.py +0 -0
  56. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/session_cost.py +0 -0
  57. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/__init__.py +0 -0
  58. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/jsonl.py +0 -0
  59. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/sessions/manager.py +0 -0
  60. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/skill_market.py +0 -0
  61. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/skills.py +0 -0
  62. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/status.py +0 -0
  63. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/todos.py +0 -0
  64. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/__init__.py +0 -0
  65. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/file_state.py +0 -0
  66. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/result.py +0 -0
  67. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/search.py +0 -0
  68. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/shell_output.py +0 -0
  69. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tools/shell_utils.py +0 -0
  70. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/__init__.py +0 -0
  71. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/compat.py +0 -0
  72. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/diff.py +0 -0
  73. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/runner.py +0 -0
  74. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/screens.py +0 -0
  75. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/state.py +0 -0
  76. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/tui/widgets.py +0 -0
  77. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/__init__.py +0 -0
  78. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/sdk.py +0 -0
  79. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/types/tool_payloads.py +0 -0
  80. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/__init__.py +0 -0
  81. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/app.py +0 -0
  82. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/ask_user_question.py +0 -0
  83. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/exit_summary.py +0 -0
  84. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/file_mentions.py +0 -0
  85. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/loading_text.py +0 -0
  86. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/local_command.py +0 -0
  87. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/markdown.py +0 -0
  88. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/model_picker.py +0 -0
  89. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/prompt_buffer.py +0 -0
  90. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/prompt_input.py +0 -0
  91. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/session_list.py +0 -0
  92. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/session_picker.py +0 -0
  93. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/skill_picker.py +0 -0
  94. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/styles.py +0 -0
  95. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/theme_picker.py +0 -0
  96. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/thinking_state.py +0 -0
  97. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/ui/welcome.py +0 -0
  98. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/update_check.py +0 -0
  99. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/usage.py +0 -0
  100. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/__init__.py +0 -0
  101. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/debug_logger.py +0 -0
  102. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/error_logger.py +0 -0
  103. {deepy_cli-0.2.14 → deepy_cli-0.2.16}/src/deepy/utils/json.py +0 -0
  104. {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.14
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.14"
3
+ version = "0.2.16"
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.14"
3
+ __version__ = "0.2.16"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -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 = {"high", "none"}
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 == "none":
248
+ if thinking is False or value in {"none", "disabled"}:
249
249
  return "none"
250
- return "high"
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 "high"
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