deepy-cli 0.2.0__tar.gz → 0.2.1__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 (83) hide show
  1. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/PKG-INFO +3 -3
  2. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/README.md +2 -2
  3. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/pyproject.toml +2 -9
  4. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/cli.py +3 -3
  6. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/agent.py +5 -1
  7. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/context.py +8 -8
  8. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/provider.py +3 -2
  9. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/replay.py +2 -2
  10. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/runner.py +2 -2
  11. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/mcp.py +8 -3
  12. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/skills.py +1 -1
  13. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/agents.py +5 -2
  14. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/builtin.py +44 -42
  15. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/result.py +1 -1
  16. deepy_cli-0.2.1/src/deepy/types/__init__.py +2 -0
  17. deepy_cli-0.2.1/src/deepy/types/sdk.py +12 -0
  18. deepy_cli-0.2.1/src/deepy/types/tool_payloads.py +15 -0
  19. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/exit_summary.py +2 -1
  20. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/local_command.py +9 -3
  21. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/message_view.py +27 -15
  22. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/prompt_input.py +7 -6
  23. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/skill_picker.py +150 -1
  24. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/terminal.py +76 -23
  25. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/welcome.py +2 -1
  26. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/utils/json.py +5 -2
  27. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/utils/notify.py +2 -2
  28. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/__main__.py +0 -0
  29. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/config/__init__.py +0 -0
  30. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/config/settings.py +0 -0
  31. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/__init__.py +0 -0
  32. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  33. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  34. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  35. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/WebFetch.md +0 -0
  36. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/WebSearch.md +0 -0
  37. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/__init__.py +0 -0
  38. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/edit.md +0 -0
  39. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/modify.md +0 -0
  40. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/read.md +0 -0
  41. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/shell.md +0 -0
  42. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/data/tools/write.md +0 -0
  43. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/errors.py +0 -0
  44. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/__init__.py +0 -0
  45. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/compaction.py +0 -0
  46. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/events.py +0 -0
  47. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/model_capabilities.py +0 -0
  48. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/llm/thinking.py +0 -0
  49. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/__init__.py +0 -0
  50. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/compact.py +0 -0
  51. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/init_agents.py +0 -0
  52. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/rules.py +0 -0
  53. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/runtime_context.py +0 -0
  54. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/system.py +0 -0
  55. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/prompts/tool_docs.py +0 -0
  56. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/sessions/__init__.py +0 -0
  57. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/sessions/jsonl.py +0 -0
  58. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/sessions/manager.py +0 -0
  59. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/skill_market.py +0 -0
  60. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/status.py +0 -0
  61. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/__init__.py +0 -0
  62. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/file_state.py +0 -0
  63. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/shell_output.py +0 -0
  64. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/tools/shell_utils.py +0 -0
  65. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/__init__.py +0 -0
  66. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/app.py +0 -0
  67. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/ask_user_question.py +0 -0
  68. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/file_mentions.py +0 -0
  69. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/loading_text.py +0 -0
  70. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/markdown.py +0 -0
  71. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/model_picker.py +0 -0
  72. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/prompt_buffer.py +0 -0
  73. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/session_list.py +0 -0
  74. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/session_picker.py +0 -0
  75. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/slash_commands.py +0 -0
  76. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/styles.py +0 -0
  77. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/theme_picker.py +0 -0
  78. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/ui/thinking_state.py +0 -0
  79. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/update_check.py +0 -0
  80. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/usage.py +0 -0
  81. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/utils/__init__.py +0 -0
  82. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/utils/debug_logger.py +0 -0
  83. {deepy_cli-0.2.0 → deepy_cli-0.2.1}/src/deepy/utils/error_logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -342,7 +342,7 @@ A concise `AGENTS.md` works best:
342
342
  ## Commands
343
343
  - Test: `uv run pytest`
344
344
  - Lint: `uv run ruff check`
345
- - Type check: `uv run pyright`
345
+ - Type check: `uv run ty check src`
346
346
 
347
347
  ## Architecture
348
348
  - Keep CLI entry points thin; put reusable behavior under `src/`.
@@ -369,7 +369,7 @@ create or refresh the project root `AGENTS.md`.
369
369
  uv sync --group dev
370
370
  uv run pytest
371
371
  uv run ruff check
372
- uv run pyright
372
+ uv run ty check src
373
373
  uv build
374
374
  ```
375
375
 
@@ -313,7 +313,7 @@ A concise `AGENTS.md` works best:
313
313
  ## Commands
314
314
  - Test: `uv run pytest`
315
315
  - Lint: `uv run ruff check`
316
- - Type check: `uv run pyright`
316
+ - Type check: `uv run ty check src`
317
317
 
318
318
  ## Architecture
319
319
  - Keep CLI entry points thin; put reusable behavior under `src/`.
@@ -340,7 +340,7 @@ create or refresh the project root `AGENTS.md`.
340
340
  uv sync --group dev
341
341
  uv run pytest
342
342
  uv run ruff check
343
- uv run pyright
343
+ uv run ty check src
344
344
  uv build
345
345
  ```
346
346
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -39,10 +39,10 @@ Issues = "https://github.com/kirineko/deepy/issues"
39
39
 
40
40
  [dependency-groups]
41
41
  dev = [
42
- "pyright>=1.1.407",
43
42
  "pytest>=8.0",
44
43
  "pytest-asyncio>=0.24",
45
44
  "ruff>=0.14",
45
+ "ty>=0.0.32",
46
46
  ]
47
47
 
48
48
  [tool.pytest.ini_options]
@@ -54,13 +54,6 @@ target-version = "py312"
54
54
  src = ["src", "tests"]
55
55
  exclude = ["dist", "reference", "spec"]
56
56
 
57
- [tool.pyright]
58
- pythonVersion = "3.12"
59
- typeCheckingMode = "off"
60
- include = ["src", "tests"]
61
- exclude = ["dist", "reference", "spec"]
62
- reportMissingTypeStubs = false
63
-
64
57
  [tool.uv.build-backend]
65
58
  module-name = "deepy"
66
59
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -4,7 +4,7 @@ import argparse
4
4
  import asyncio
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Sequence
7
+ from typing import Any, Sequence
8
8
 
9
9
  import tomli_w
10
10
 
@@ -198,7 +198,7 @@ def _cmd_config_theme(args: argparse.Namespace) -> int:
198
198
  return 0
199
199
 
200
200
 
201
- def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
201
+ def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
202
202
  settings = load_settings(args.config)
203
203
  checks: list[dict[str, object]] = []
204
204
 
@@ -251,7 +251,7 @@ def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
251
251
  }
252
252
 
253
253
 
254
- async def _doctor_live(settings: Settings) -> dict[str, object]:
254
+ async def _doctor_live(settings: Settings) -> dict[str, Any]:
255
255
  from agents import Agent, RunConfig, Runner
256
256
 
257
257
  provider = build_provider_bundle(settings)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from deepy.config import Settings
6
7
  from deepy.prompts import build_system_prompt
@@ -10,6 +11,9 @@ from deepy.tools.agents import build_function_tools
10
11
 
11
12
  from .provider import ProviderBundle, build_provider_bundle
12
13
 
14
+ if TYPE_CHECKING:
15
+ from agents.mcp import MCPServer
16
+
13
17
 
14
18
  def build_deepy_agent(
15
19
  settings: Settings,
@@ -18,7 +22,7 @@ def build_deepy_agent(
18
22
  project_root: Path,
19
23
  provider: ProviderBundle | None = None,
20
24
  loaded_skills: list[SkillInfo] | None = None,
21
- mcp_servers: list[object] | None = None,
25
+ mcp_servers: list[MCPServer] | None = None,
22
26
  preferred_mcp_web_search_tools: list[str] | None = None,
23
27
  ):
24
28
  from agents import Agent
@@ -1,16 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable
4
3
  from math import ceil
5
4
  from typing import Any
6
5
 
7
6
  from deepy.config import Settings
7
+ from deepy.types.sdk import SessionInputCallback
8
8
  from deepy.utils import json as json_utils
9
9
 
10
+ tiktoken: Any | None
10
11
  try:
11
- import tiktoken
12
+ import tiktoken as _tiktoken
12
13
  except Exception: # pragma: no cover - optional dependency fallback.
13
- tiktoken = None # type: ignore[assignment]
14
+ tiktoken = None
15
+ else:
16
+ tiktoken = _tiktoken
14
17
 
15
18
  _ENCODING = None
16
19
 
@@ -38,11 +41,8 @@ def estimate_tokens_for_items(items: list[dict[str, Any]]) -> int:
38
41
  return sum(estimate_tokens_for_item(item) for item in items)
39
42
 
40
43
 
41
- def build_session_input_callback(settings: Settings) -> Callable[
42
- [list[dict[str, Any]], list[dict[str, Any]]],
43
- list[dict[str, Any]],
44
- ]:
45
- def callback(history: list[dict[str, Any]], new_input: list[dict[str, Any]]) -> list[dict[str, Any]]:
44
+ def build_session_input_callback(settings: Settings) -> SessionInputCallback:
45
+ def callback(history: list[Any], new_input: list[Any]) -> list[Any]:
46
46
  return [*history, *new_input]
47
47
 
48
48
  return callback
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
+ from agents import Model, ModelSettings
6
7
  from agents import OpenAIChatCompletionsModel
7
8
 
8
9
  from deepy.config import Settings
@@ -17,8 +18,8 @@ from .replay import (
17
18
  @dataclass(frozen=True)
18
19
  class ProviderBundle:
19
20
  client: object
20
- model: object
21
- model_settings: object
21
+ model: Model
22
+ model_settings: ModelSettings
22
23
 
23
24
 
24
25
  class DeepyOpenAIChatCompletionsModel(OpenAIChatCompletionsModel):
@@ -105,9 +105,9 @@ def sanitize_chat_completion_stream_event(event: Any) -> Any | None:
105
105
  if getattr(event, "type", None) == "response.completed":
106
106
  response = getattr(event, "response", None)
107
107
  output = getattr(response, "output", None)
108
- if isinstance(output, list):
108
+ if response is not None and isinstance(output, list):
109
109
  try:
110
- response.output = sanitize_model_response_output(output)
110
+ cast(Any, response).output = sanitize_model_response_output(output)
111
111
  except Exception:
112
112
  pass
113
113
  return event
@@ -130,7 +130,7 @@ async def run_prompt_once(
130
130
  input=prompt,
131
131
  max_turns=max_turns,
132
132
  run_config=run_config,
133
- session=session,
133
+ session=session, # ty: ignore[invalid-argument-type] - DeepyJsonlSession matches the SDK Session protocol at runtime.
134
134
  )
135
135
  if should_interrupt is not None:
136
136
  interrupt_task = asyncio.create_task(
@@ -349,7 +349,7 @@ def _max_turns_output(chunks: list[str], *, max_turns: int) -> str:
349
349
 
350
350
  def format_deepseek_api_error(error: Any) -> str:
351
351
  status_code = _safe_int(getattr(error, "status_code", None))
352
- status = DEEPSEEK_ERROR_CODES.get(status_code)
352
+ status = DEEPSEEK_ERROR_CODES.get(status_code) if status_code is not None else None
353
353
  title = f"DeepSeek API error {status_code}" if status_code is not None else "DeepSeek API error"
354
354
  if status is not None:
355
355
  title = f"{title}: {status.title}"
@@ -155,7 +155,12 @@ class DeepyMcpRuntime:
155
155
  await self._manager.cleanup_all()
156
156
 
157
157
  def _build_sdk_servers(self) -> list[Any]:
158
- from agents.mcp import MCPServerStdio, MCPServerStreamableHttp
158
+ from agents.mcp import (
159
+ MCPServerStdio,
160
+ MCPServerStdioParams,
161
+ MCPServerStreamableHttp,
162
+ MCPServerStreamableHttpParams,
163
+ )
159
164
 
160
165
  servers: list[Any] = []
161
166
  for definition in self.definitions:
@@ -173,7 +178,7 @@ class DeepyMcpRuntime:
173
178
  )
174
179
  continue
175
180
  if definition.transport == "stdio":
176
- params: dict[str, Any] = {"command": definition.command or ""}
181
+ params: MCPServerStdioParams = {"command": definition.command or ""}
177
182
  if definition.args:
178
183
  params["args"] = list(definition.args)
179
184
  if definition.env:
@@ -189,7 +194,7 @@ class DeepyMcpRuntime:
189
194
  ),
190
195
  )
191
196
  else:
192
- params = {"url": definition.url or ""}
197
+ params: MCPServerStreamableHttpParams = {"url": definition.url or ""}
193
198
  if definition.headers:
194
199
  params["headers"] = dict(definition.headers)
195
200
  server = MCPServerStreamableHttp(
@@ -116,7 +116,7 @@ def _format_skills(skills: Iterable[SkillInfo], *, include_paths: bool) -> str:
116
116
 
117
117
 
118
118
  def _builtin_skills_root() -> Path:
119
- return Path(resources.files("deepy.data").joinpath("skills"))
119
+ return Path(str(resources.files("deepy.data").joinpath("skills")))
120
120
 
121
121
 
122
122
  def _discover_skills_root(root: Path, *, scope: str) -> list[SkillInfo]:
@@ -1,17 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from deepy.utils import json as json_utils
6
6
 
7
7
  from .builtin import ToolRuntime
8
8
 
9
+ if TYPE_CHECKING:
10
+ from agents import Tool
11
+
9
12
 
10
13
  def build_function_tools(
11
14
  runtime: ToolRuntime,
12
15
  *,
13
16
  preferred_mcp_web_search_tools: list[str] | None = None,
14
- ) -> list[object]:
17
+ ) -> list[Tool]:
15
18
  from agents.tool import FunctionTool
16
19
 
17
20
  async def invoke_shell(_context: object, raw_input: str) -> str:
@@ -22,6 +22,7 @@ from html.parser import HTMLParser
22
22
  from pathlib import Path
23
23
 
24
24
  from deepy.config import DEFAULT_WEB_SEARCH_SEARXNG_URL, Settings, mask_secret
25
+ from deepy.types.tool_payloads import AskUserOption, AskUserQuestion
25
26
  from deepy.utils import json as json_utils
26
27
 
27
28
  from .file_state import FileSnippet, FileState
@@ -728,7 +729,7 @@ def _decide_search_language_with_llm(query: str, settings: Settings) -> dict[str
728
729
  )
729
730
  parsed = _parse_json_response(_web_search_chat(settings, prompt))
730
731
  dominant_language = parsed.get("dominant_language")
731
- if dominant_language not in {"en", "zh"}:
732
+ if not isinstance(dominant_language, str) or dominant_language not in {"en", "zh"}:
732
733
  raise ValueError(f"Unexpected dominant language: {dominant_language}")
733
734
  reason = parsed.get("reason")
734
735
  return {
@@ -1249,7 +1250,7 @@ class ToolRuntime:
1249
1250
  )
1250
1251
  self.file_state.mark_read(target, full=False)
1251
1252
  snippet_metadata = _snippet_metadata(snippet)
1252
- metadata = {
1253
+ metadata: dict[str, object] = {
1253
1254
  "path": str(target),
1254
1255
  "kind": "file",
1255
1256
  "startLine": start + 1,
@@ -1322,7 +1323,7 @@ class ToolRuntime:
1322
1323
  encoding = (
1323
1324
  existing_metadata.encoding
1324
1325
  if existing_metadata is not None
1325
- else _default_new_text_encoding(text_content, platform_name=self.platform_name)
1326
+ else _default_new_text_encoding()
1326
1327
  )
1327
1328
  line_endings = _detect_line_endings(old_content or text_content)
1328
1329
  normalized_content = _normalize_line_endings(text_content, line_endings)
@@ -1453,7 +1454,7 @@ class ToolRuntime:
1453
1454
  _write_text_with_encoding(target, updated, text_metadata.encoding)
1454
1455
  self.file_state.mark_written(target)
1455
1456
  diff = _unified_diff(text, updated, path=str(target))
1456
- metadata = {
1457
+ metadata: dict[str, object] = {
1457
1458
  "path": str(target),
1458
1459
  "file_path": str(target),
1459
1460
  "occurrences": occurrences if replace_all else 1,
@@ -1542,7 +1543,6 @@ class ToolRuntime:
1542
1543
  self.cwd = final_cwd
1543
1544
  returncode = exit_code if exit_code is not None else process.returncode
1544
1545
  output, output_truncated = _truncate_output(stdout + (stderr or ""))
1545
- result = ToolResult.ok_result if returncode == 0 else ToolResult.error_result
1546
1546
  metadata = _shell_metadata(
1547
1547
  self.cwd,
1548
1548
  process_id,
@@ -1558,12 +1558,12 @@ class ToolRuntime:
1558
1558
  }
1559
1559
  )
1560
1560
  if returncode == 0:
1561
- return result(
1561
+ return ToolResult.ok_result(
1562
1562
  name,
1563
1563
  output,
1564
1564
  metadata=metadata,
1565
1565
  ).to_json()
1566
- return result(
1566
+ return ToolResult.error_result(
1567
1567
  name,
1568
1568
  f"Command exited with code {returncode}.",
1569
1569
  output=output,
@@ -1916,17 +1916,10 @@ def _python_text_encoding(encoding: str) -> str:
1916
1916
  return "utf8"
1917
1917
 
1918
1918
 
1919
- def _default_new_text_encoding(content: str, *, platform_name: str | None = None) -> str:
1920
- resolved_platform = platform_name or sys.platform
1921
- if resolved_platform.startswith("win") and _contains_non_ascii(content):
1922
- return "utf8-sig"
1919
+ def _default_new_text_encoding() -> str:
1923
1920
  return "utf8"
1924
1921
 
1925
1922
 
1926
- def _contains_non_ascii(text: str) -> bool:
1927
- return any(ord(char) > 0x7F for char in text)
1928
-
1929
-
1930
1923
  def _write_text_with_encoding(path: Path, content: str, encoding: str) -> None:
1931
1924
  path.write_bytes(content.encode(_python_text_encoding(encoding)))
1932
1925
 
@@ -1974,24 +1967,24 @@ def _format_notebook(path: Path) -> tuple[str, str | None]:
1974
1967
  cells = parsed.get("cells")
1975
1968
  lines: list[str] = []
1976
1969
  if isinstance(cells, list):
1977
- for index, cell in enumerate(cells):
1978
- if not isinstance(cell, dict):
1970
+ for index, raw_cell in enumerate(cells):
1971
+ cell = _string_key_dict(raw_cell)
1972
+ if cell is None:
1979
1973
  continue
1980
- cell_type = cell.get("cell_type") if isinstance(cell.get("cell_type"), str) else "unknown"
1974
+ raw_cell_type = cell.get("cell_type")
1975
+ cell_type = raw_cell_type if isinstance(raw_cell_type, str) else "unknown"
1981
1976
  lines.append(f"# Cell {index + 1} ({cell_type})")
1982
1977
  lines.extend(_normalize_notebook_field(cell.get("source")))
1983
1978
 
1984
1979
  outputs = cell.get("outputs")
1985
1980
  if not isinstance(outputs, list):
1986
1981
  continue
1987
- for output_index, output in enumerate(outputs):
1988
- if not isinstance(output, dict):
1982
+ for output_index, raw_output in enumerate(outputs):
1983
+ output = _string_key_dict(raw_output)
1984
+ if output is None:
1989
1985
  continue
1990
- output_type = (
1991
- output.get("output_type")
1992
- if isinstance(output.get("output_type"), str)
1993
- else "output"
1994
- )
1986
+ raw_output_type = output.get("output_type")
1987
+ output_type = raw_output_type if isinstance(raw_output_type, str) else "output"
1995
1988
  lines.append(f"# Output {output_index + 1} ({output_type})")
1996
1989
  lines.extend(_format_notebook_output(output))
1997
1990
 
@@ -2011,12 +2004,13 @@ def _normalize_notebook_field(value: object) -> list[str]:
2011
2004
  def _format_notebook_output(output: dict[str, object]) -> list[str]:
2012
2005
  lines = _normalize_notebook_field(output.get("text"))
2013
2006
  data = output.get("data")
2014
- if isinstance(data, dict):
2015
- lines.extend(_normalize_notebook_field(data.get("text/plain")))
2016
- image_png = data.get("image/png")
2007
+ data_dict = _string_key_dict(data)
2008
+ if data_dict is not None:
2009
+ lines.extend(_normalize_notebook_field(data_dict.get("text/plain")))
2010
+ image_png = data_dict.get("image/png")
2017
2011
  if isinstance(image_png, str):
2018
2012
  lines.append(f"[image/png {len(image_png)} chars]")
2019
- image_jpeg = data.get("image/jpeg")
2013
+ image_jpeg = data_dict.get("image/jpeg")
2020
2014
  if isinstance(image_jpeg, str):
2021
2015
  lines.append(f"[image/jpeg {len(image_jpeg)} chars]")
2022
2016
  traceback = output.get("traceback")
@@ -2025,6 +2019,14 @@ def _format_notebook_output(output: dict[str, object]) -> list[str]:
2025
2019
  return lines or ["[output omitted]"]
2026
2020
 
2027
2021
 
2022
+ def _string_key_dict(value: object) -> dict[str, object] | None:
2023
+ if not isinstance(value, dict):
2024
+ return None
2025
+ if not all(isinstance(key, str) for key in value):
2026
+ return None
2027
+ return {key: item for key, item in value.items() if isinstance(key, str)}
2028
+
2029
+
2028
2030
  @dataclass(frozen=True)
2029
2031
  class PageRange:
2030
2032
  start: int
@@ -2360,7 +2362,7 @@ def _now_iso() -> str:
2360
2362
  return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
2361
2363
 
2362
2364
 
2363
- def _terminate_process(process: subprocess.Popen[str]) -> None:
2365
+ def _terminate_process(process: subprocess.Popen[bytes]) -> None:
2364
2366
  try:
2365
2367
  if os.name != "nt":
2366
2368
  os.killpg(process.pid, signal.SIGKILL)
@@ -2483,13 +2485,14 @@ def _gitignore_pattern_matches(pattern: str, relative_path: str, is_dir: bool) -
2483
2485
  return any(fnmatch(part, normalized_pattern) for part in parts)
2484
2486
 
2485
2487
 
2486
- def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], str | None]:
2488
+ def _parse_ask_user_questions(value: object) -> tuple[list[AskUserQuestion], str | None]:
2487
2489
  if not isinstance(value, list) or not value:
2488
2490
  return [], '"questions" must be a non-empty array.'
2489
2491
 
2490
- questions: list[dict[str, object]] = []
2491
- for index, item in enumerate(value):
2492
- if not isinstance(item, dict):
2492
+ questions: list[AskUserQuestion] = []
2493
+ for index, raw_item in enumerate(value):
2494
+ item = _string_key_dict(raw_item)
2495
+ if item is None:
2493
2496
  return [], f"Question at index {index} must be an object."
2494
2497
 
2495
2498
  question = _trimmed_string(item.get("question"))
@@ -2500,9 +2503,10 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
2500
2503
  if not isinstance(raw_options, list) or not raw_options:
2501
2504
  return [], f'Question at index {index} must include a non-empty "options" array.'
2502
2505
 
2503
- options: list[dict[str, str]] = []
2504
- for option_index, option in enumerate(raw_options):
2505
- if not isinstance(option, dict):
2506
+ options: list[AskUserOption] = []
2507
+ for option_index, raw_option in enumerate(raw_options):
2508
+ option = _string_key_dict(raw_option)
2509
+ if option is None:
2506
2510
  return [], f"Option {option_index} for question {index} must be an object."
2507
2511
 
2508
2512
  label = _trimmed_string(option.get("label"))
@@ -2512,13 +2516,13 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
2512
2516
  f'Option {option_index} for question {index} is missing a non-empty "label" string.',
2513
2517
  )
2514
2518
 
2515
- parsed_option = {"label": label}
2519
+ parsed_option: AskUserOption = {"label": label}
2516
2520
  description = _trimmed_string(option.get("description"))
2517
2521
  if description:
2518
2522
  parsed_option["description"] = description
2519
2523
  options.append(parsed_option)
2520
2524
 
2521
- parsed_question: dict[str, object] = {
2525
+ parsed_question: AskUserQuestion = {
2522
2526
  "question": question,
2523
2527
  "options": options,
2524
2528
  }
@@ -2530,15 +2534,13 @@ def _parse_ask_user_questions(value: object) -> tuple[list[dict[str, object]], s
2530
2534
  return questions, None
2531
2535
 
2532
2536
 
2533
- def _build_question_summary(questions: list[dict[str, object]]) -> str:
2537
+ def _build_question_summary(questions: list[AskUserQuestion]) -> str:
2534
2538
  lines = ["Waiting for user input."]
2535
2539
  for index, item in enumerate(questions):
2536
2540
  lines.append("")
2537
2541
  lines.append(f"{index + 1}. {item['question']}")
2538
2542
  lines.append(f" Mode: {'multi-select' if item.get('multiSelect') else 'single-select'}")
2539
2543
  for option in item["options"]:
2540
- if not isinstance(option, dict):
2541
- continue
2542
2544
  lines.append(f" - {option['label']}")
2543
2545
  if option.get("description"):
2544
2546
  lines.append(f" {option['description']}")
@@ -17,7 +17,7 @@ class ToolResult:
17
17
  followUpMessages: list[dict[str, Any]] | None = None
18
18
 
19
19
  def to_dict(self) -> dict[str, Any]:
20
- payload = {
20
+ payload: dict[str, Any] = {
21
21
  "ok": self.ok,
22
22
  "name": self.name,
23
23
  "output": self.output,
@@ -0,0 +1,2 @@
1
+ """Shared Deepy type definitions."""
2
+
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+
5
+ from agents.items import TResponseInputItem
6
+
7
+
8
+ SessionInputCallback = Callable[
9
+ [list[TResponseInputItem], list[TResponseInputItem]],
10
+ list[TResponseInputItem],
11
+ ]
12
+
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NotRequired, TypedDict
4
+
5
+
6
+ class AskUserOption(TypedDict):
7
+ label: str
8
+ description: NotRequired[str]
9
+
10
+
11
+ class AskUserQuestion(TypedDict):
12
+ question: str
13
+ options: list[AskUserOption]
14
+ multiSelect: NotRequired[bool]
15
+
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from collections.abc import Sequence
4
5
  from typing import Any, Mapping
5
6
 
6
7
 
@@ -55,7 +56,7 @@ def extract_usage_fields(usage: Any) -> UsageFields:
55
56
  def build_exit_summary_text(
56
57
  *,
57
58
  session: Any | None = None,
58
- messages: list[Mapping[str, Any]] | None = None,
59
+ messages: Sequence[Mapping[str, Any]] | None = None,
59
60
  model: str | None = None,
60
61
  ) -> str:
61
62
  usage = extract_usage_fields(_get_usage(session))
@@ -22,10 +22,13 @@ from deepy.tools.shell_output import decode_shell_output_bytes
22
22
  from deepy.tools.shell_utils import RuntimeEnvironment, detect_runtime_environment
23
23
  from deepy.utils import json as json_utils
24
24
 
25
+ pty: Any | None
25
26
  try:
26
- import pty
27
+ import pty as _pty
27
28
  except ImportError: # pragma: no cover - exercised on Windows.
28
- pty = None # type: ignore[assignment]
29
+ pty = None
30
+ else:
31
+ pty = _pty
29
32
 
30
33
  DEFAULT_LOCAL_COMMAND_TIMEOUT_MS = 120_000
31
34
  DEFAULT_DISPLAY_OUTPUT_LIMIT = 30_000
@@ -389,9 +392,12 @@ def _read_pipe_output(
389
392
  stream = process.stdout
390
393
  if stream is None:
391
394
  return
395
+ read1 = getattr(stream, "read1", None)
396
+ if not callable(read1):
397
+ return
392
398
  while True:
393
399
  try:
394
- chunk = stream.read1(4096)
400
+ chunk = read1(4096)
395
401
  except Exception:
396
402
  return
397
403
  if not chunk: