deepagents-code 0.1.87__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (91) hide show
  1. deepagents_code/_ask_user_types.py +1 -1
  2. deepagents_code/_build_info.py +1 -1
  3. deepagents_code/_dep_floor_check.py +3 -3
  4. deepagents_code/_git.py +12 -6
  5. deepagents_code/_repository_bounds.py +1 -1
  6. deepagents_code/_session_stats.py +1 -1
  7. deepagents_code/_tool_stream.py +1 -1
  8. deepagents_code/_version.py +1 -1
  9. deepagents_code/agent.py +3 -0
  10. deepagents_code/app.py +111 -37
  11. deepagents_code/approval_mode.py +1 -1
  12. deepagents_code/auto_mode.py +4 -4
  13. deepagents_code/btw.py +2 -2
  14. deepagents_code/btw_api.py +2 -2
  15. deepagents_code/built_in_skills/deepagents-thread-inspector/scripts/inspect_sessions.py +3 -11
  16. deepagents_code/client/launch/server.py +1 -1
  17. deepagents_code/client/launch/server_manager.py +1 -1
  18. deepagents_code/client/non_interactive.py +1 -1
  19. deepagents_code/client/remote_client.py +3 -3
  20. deepagents_code/config.py +7 -7
  21. deepagents_code/config_manifest.py +11 -7
  22. deepagents_code/configurable_model.py +1 -1
  23. deepagents_code/configuration/paths.py +4 -1
  24. deepagents_code/configuration/providers.py +3 -3
  25. deepagents_code/configuration/resolver.py +1 -1
  26. deepagents_code/cost_tracking.py +1 -1
  27. deepagents_code/doctor.py +1 -1
  28. deepagents_code/event_bus.py +1 -1
  29. deepagents_code/extensions/discovery.py +1 -1
  30. deepagents_code/extensions/loader.py +1 -1
  31. deepagents_code/extensions/runtime.py +1 -1
  32. deepagents_code/extras_info.py +12 -12
  33. deepagents_code/file_ops.py +3 -3
  34. deepagents_code/formatting.py +1 -1
  35. deepagents_code/goal_rubric.py +1 -1
  36. deepagents_code/hooks/migration.py +2 -2
  37. deepagents_code/hooks/runner.py +2 -2
  38. deepagents_code/hooks/transcript.py +2 -2
  39. deepagents_code/hooks/trust.py +3 -3
  40. deepagents_code/input.py +7 -5
  41. deepagents_code/integrations/sandbox_factory.py +2 -2
  42. deepagents_code/iterm_cursor_guide.py +1 -1
  43. deepagents_code/main.py +153 -84
  44. deepagents_code/managed_tools.py +1 -1
  45. deepagents_code/mcp_auth.py +1 -1
  46. deepagents_code/mcp_tools.py +2 -2
  47. deepagents_code/media_utils.py +3 -2
  48. deepagents_code/memory_guard.py +4 -4
  49. deepagents_code/model_api.py +3 -3
  50. deepagents_code/model_config.py +23 -23
  51. deepagents_code/model_retry.py +2 -2
  52. deepagents_code/offload.py +5 -5
  53. deepagents_code/offload_middleware.py +1 -1
  54. deepagents_code/plugins/adapters/mcp.py +1 -1
  55. deepagents_code/plugins/adapters/skills.py +1 -1
  56. deepagents_code/plugins/adapters/skills_middleware.py +2 -2
  57. deepagents_code/plugins/manifest.py +1 -1
  58. deepagents_code/plugins/marketplace.py +4 -0
  59. deepagents_code/sessions.py +4 -4
  60. deepagents_code/skills/commands.py +1 -1
  61. deepagents_code/skills/load.py +1 -1
  62. deepagents_code/skills/trust.py +4 -4
  63. deepagents_code/terminal_escape.py +1 -1
  64. deepagents_code/terminal_title.py +1 -1
  65. deepagents_code/theme.py +1 -1
  66. deepagents_code/tool_catalog.py +2 -2
  67. deepagents_code/tool_display.py +6 -4
  68. deepagents_code/tui/key_hints.py +4 -1
  69. deepagents_code/tui/modals/plugin_manager/content.py +7 -2
  70. deepagents_code/tui/modals/plugin_manager/models.py +3 -2
  71. deepagents_code/tui/modals/plugin_manager/state.py +1 -1
  72. deepagents_code/tui/textual_adapter.py +2 -2
  73. deepagents_code/tui/widgets/_links.py +2 -2
  74. deepagents_code/tui/widgets/autocomplete.py +1 -1
  75. deepagents_code/tui/widgets/debug_console.py +1 -1
  76. deepagents_code/tui/widgets/history.py +1 -1
  77. deepagents_code/tui/widgets/messages.py +5 -5
  78. deepagents_code/tui/widgets/model_selector.py +2 -2
  79. deepagents_code/tui/widgets/startup_tip.py +1 -6
  80. deepagents_code/tui/widgets/status.py +1 -1
  81. deepagents_code/tui/widgets/subagent_panel.py +6 -6
  82. deepagents_code/tui/widgets/thread_selector.py +3 -3
  83. deepagents_code/tui/widgets/tool_widgets.py +1 -1
  84. deepagents_code/tui/widgets/welcome.py +2 -2
  85. deepagents_code/update_check.py +642 -158
  86. {deepagents_code-0.1.87.dist-info → deepagents_code-0.2.0.dist-info}/METADATA +30 -32
  87. {deepagents_code-0.1.87.dist-info → deepagents_code-0.2.0.dist-info}/RECORD +91 -91
  88. {deepagents_code-0.1.87.data → deepagents_code-0.2.0.data}/data/deepagents_code/default_agent_prompt.md +0 -0
  89. {deepagents_code-0.1.87.dist-info → deepagents_code-0.2.0.dist-info}/WHEEL +0 -0
  90. {deepagents_code-0.1.87.dist-info → deepagents_code-0.2.0.dist-info}/entry_points.txt +0 -0
  91. {deepagents_code-0.1.87.dist-info → deepagents_code-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -109,7 +109,7 @@ def decode_multi_select_answer(raw: str) -> list[str] | None:
109
109
  """
110
110
  try:
111
111
  decoded = json.loads(raw)
112
- except (json.JSONDecodeError, RecursionError):
112
+ except json.JSONDecodeError, RecursionError:
113
113
  return None
114
114
  if not isinstance(decoded, list) or not all(
115
115
  isinstance(value, str) for value in decoded
@@ -1,3 +1,3 @@
1
1
  """Generated at build time. Do not edit or commit."""
2
2
 
3
- BUILD_COMMIT = "d1962cf"
3
+ BUILD_COMMIT = "981a87a"
@@ -96,7 +96,7 @@ def _load_cli_requirements() -> list[str] | None:
96
96
  try:
97
97
  data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
98
98
  entries = data["project"]["dependencies"]
99
- except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError):
99
+ except OSError, KeyError, TypeError, tomllib.TOMLDecodeError:
100
100
  logger.debug(
101
101
  "Could not read dependency floors from the source checkout",
102
102
  exc_info=True,
@@ -330,7 +330,7 @@ def _workspace_editable_paths() -> list[Path]:
330
330
  try:
331
331
  data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
332
332
  sources = data["tool"]["uv"]["sources"]
333
- except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError):
333
+ except OSError, KeyError, TypeError, tomllib.TOMLDecodeError:
334
334
  logger.debug(
335
335
  "Could not read workspace sources from the source checkout",
336
336
  exc_info=True,
@@ -565,7 +565,7 @@ def _read_dismissed_fingerprints(path: Path) -> dict[str, str]:
565
565
  """
566
566
  try:
567
567
  data = json.loads(path.read_text(encoding="utf-8"))
568
- except (OSError, ValueError):
568
+ except OSError, ValueError:
569
569
  return {}
570
570
  if not isinstance(data, dict):
571
571
  return {}
deepagents_code/_git.py CHANGED
@@ -173,7 +173,7 @@ def _read_single_git_metadata_line(path: Path) -> str | None:
173
173
  return None
174
174
  try:
175
175
  raw = path.read_text(encoding="utf-8")
176
- except (OSError, UnicodeError):
176
+ except OSError, UnicodeError:
177
177
  logger.debug("Failed to read Git metadata from %s", path, exc_info=True)
178
178
  return None
179
179
 
@@ -204,7 +204,7 @@ def _resolve_git_metadata_path(path: Path, *, relative_to: Path) -> Path | None:
204
204
  candidate = relative_to / candidate
205
205
  try:
206
206
  return candidate.resolve(strict=True)
207
- except (OSError, RuntimeError, ValueError):
207
+ except OSError, RuntimeError, ValueError:
208
208
  logger.debug("Failed to resolve Git metadata path from %s", path, exc_info=True)
209
209
  return None
210
210
 
@@ -227,7 +227,7 @@ def _normalize_git_metadata_path(path: Path, *, relative_to: Path) -> Path | Non
227
227
  candidate = relative_to / candidate
228
228
  try:
229
229
  return Path(os.path.abspath(candidate)) # noqa: PTH100 # do not follow links
230
- except (OSError, RuntimeError, ValueError):
230
+ except OSError, RuntimeError, ValueError:
231
231
  logger.debug(
232
232
  "Failed to normalize Git metadata path from %s", path, exc_info=True
233
233
  )
@@ -254,7 +254,7 @@ def _parse_trusted_git_dir_pointer(git_entry: Path) -> Path | None:
254
254
  candidate = git_entry.parent / candidate
255
255
  try:
256
256
  git_dir = candidate.resolve(strict=True)
257
- except (OSError, RuntimeError, ValueError):
257
+ except OSError, RuntimeError, ValueError:
258
258
  logger.debug(
259
259
  "Failed to resolve trusted gitdir pointer %s", git_entry, exc_info=True
260
260
  )
@@ -328,7 +328,7 @@ def find_git_common_dir(path: str | Path) -> Path | None:
328
328
  backlink = _normalize_git_metadata_path(git_dir / "gitdir", relative_to=git_dir)
329
329
  if backlink is None or backlink != git_entry:
330
330
  return None
331
- except (OSError, RuntimeError, ValueError):
331
+ except OSError, RuntimeError, ValueError:
332
332
  logger.debug(
333
333
  "Failed to validate Git common directory for %s", path, exc_info=True
334
334
  )
@@ -377,7 +377,7 @@ def _submodule_git_dir(root: Path, parent_config_dir: Path) -> Path | None:
377
377
  if not candidate.is_absolute():
378
378
  candidate = git_dir / candidate
379
379
  return git_dir if candidate.resolve(strict=True) == root else None
380
- except (OSError, RuntimeError, ValueError):
380
+ except OSError, RuntimeError, ValueError:
381
381
  logger.debug("Failed to validate submodule Git metadata")
382
382
  return None
383
383
 
@@ -451,6 +451,9 @@ def read_git_branch_via_subprocess(path: str | Path) -> str:
451
451
  ["git", "rev-parse", "--abbrev-ref", "HEAD"], # noqa: S607
452
452
  capture_output=True,
453
453
  text=True,
454
+ # Branch names are UTF-8, not in the Windows ANSI code page.
455
+ encoding="utf-8",
456
+ errors="replace",
454
457
  timeout=2,
455
458
  check=False,
456
459
  cwd=path,
@@ -653,6 +656,9 @@ def read_git_remote_url_via_subprocess(path: str | Path) -> str:
653
656
  ["git", "config", "--get", "remote.origin.url"], # noqa: S607
654
657
  capture_output=True,
655
658
  text=True,
659
+ # Local-path remotes are UTF-8, not in the Windows ANSI code page.
660
+ encoding="utf-8",
661
+ errors="replace",
656
662
  timeout=2,
657
663
  check=False,
658
664
  cwd=path,
@@ -432,7 +432,7 @@ class RepositoryBounds:
432
432
  body, separator, notes = content.partition("\n\n")
433
433
  try:
434
434
  paths = ast.literal_eval(body)
435
- except (SyntaxError, ValueError):
435
+ except SyntaxError, ValueError:
436
436
  return content
437
437
  if not isinstance(paths, list) or not all(
438
438
  isinstance(path, str) for path in paths
@@ -1311,7 +1311,7 @@ def record_model_usage_event(
1311
1311
  "model_provider": provider,
1312
1312
  },
1313
1313
  )
1314
- except (TypeError, ValueError):
1314
+ except TypeError, ValueError:
1315
1315
  logger.debug("Rejected malformed nested model usage", exc_info=True)
1316
1316
  return None
1317
1317
  return record_message_usage(
@@ -711,7 +711,7 @@ class ToolCallBuffer:
711
711
  joined = self._materialize_args()
712
712
  try:
713
713
  parsed = json.loads(joined)
714
- except (json.JSONDecodeError, RecursionError):
714
+ except json.JSONDecodeError, RecursionError:
715
715
  # `RecursionError` is caught alongside the decode error because
716
716
  # pathologically nested model output can make `json.loads` exceed
717
717
  # the interpreter recursion limit, and that is one malformed call to
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep the `x-release-please-version` annotation — release-please uses it to
4
4
  # bump `__version__` in sync with `pyproject.toml` on every release PR.
5
- __version__ = "0.1.87" # x-release-please-version
5
+ __version__ = "0.2.0" # x-release-please-version
6
6
 
7
7
  DOCS_URL = "https://docs.langchain.com/oss/python/deepagents/code"
8
8
  """URL for `deepagents-code` documentation."""
deepagents_code/agent.py CHANGED
@@ -1160,6 +1160,7 @@ def load_async_subagents(config_path: Path | None = None) -> list[AsyncSubAgent]
1160
1160
  description = "Research agent"
1161
1161
  url = "https://my-deployment.langsmith.dev"
1162
1162
  graph_id = "agent"
1163
+ webhook = "https://example.com/webhooks/subagent-finished"
1163
1164
  ```
1164
1165
 
1165
1166
  Args:
@@ -1225,6 +1226,8 @@ def load_async_subagents(config_path: Path | None = None) -> list[AsyncSubAgent]
1225
1226
  }
1226
1227
  if "url" in spec and isinstance(spec["url"], str):
1227
1228
  agent["url"] = spec["url"]
1229
+ if "webhook" in spec and isinstance(spec["webhook"], str):
1230
+ agent["webhook"] = spec["webhook"]
1228
1231
  if "headers" in spec and isinstance(spec["headers"], dict):
1229
1232
  agent["headers"] = spec["headers"]
1230
1233
  agents.append(agent)
deepagents_code/app.py CHANGED
@@ -514,7 +514,7 @@ def _coerce_session_cost_usd(value: object) -> float:
514
514
  return 0.0
515
515
  try:
516
516
  cost_usd = float(value)
517
- except (OverflowError, ValueError):
517
+ except OverflowError, ValueError:
518
518
  logger.warning("Discarding a persisted session cost that is out of range.")
519
519
  return 0.0
520
520
  if not math.isfinite(cost_usd) or cost_usd < 0:
@@ -558,7 +558,7 @@ def _coerce_provisional_cost_delta_usd(value: object) -> float | None:
558
558
  return None
559
559
  try:
560
560
  delta_usd = float(value)
561
- except (OverflowError, ValueError):
561
+ except OverflowError, ValueError:
562
562
  logger.warning("Discarding a provisional cost delta that is out of range.")
563
563
  return None
564
564
  if not math.isfinite(delta_usd):
@@ -5642,7 +5642,7 @@ class DeepAgentsApp(App):
5642
5642
  root = await asyncio.to_thread(project_root_for, Path(self._cwd))
5643
5643
  config_path = project_hooks_path(root)
5644
5644
  has_project_hooks = await asyncio.to_thread(config_path.is_file)
5645
- except (OSError, ValueError):
5645
+ except OSError, ValueError:
5646
5646
  logger.warning(
5647
5647
  "Could not inspect project hooks after cwd switch",
5648
5648
  exc_info=True,
@@ -6687,7 +6687,7 @@ class DeepAgentsApp(App):
6687
6687
 
6688
6688
  try:
6689
6689
  messages = self.query_one("#messages", Container)
6690
- except (NoMatches, ScreenStackError):
6690
+ except NoMatches, ScreenStackError:
6691
6691
  return
6692
6692
  if not messages.is_attached:
6693
6693
  return
@@ -7692,17 +7692,30 @@ class DeepAgentsApp(App):
7692
7692
  await self._mount_message(
7693
7693
  AppMessage(f"Update log: {format_log_follow_command(log_path)}"),
7694
7694
  )
7695
+
7696
+ notices: list[str] = []
7697
+ deferred: list[bool] = []
7698
+ unverified: list[bool] = []
7699
+
7700
+ async def show_upgrade_notice(message: str) -> None:
7701
+ notices.append(message)
7702
+ await self._mount_message(AppMessage(message))
7703
+
7704
+ # A Python migration rebuilds the environment this session imports
7705
+ # from, so the TUI defers it to `dcode update` from the shell.
7695
7706
  success, output, installed = await perform_upgrade(
7707
+ on_notice=show_upgrade_notice,
7696
7708
  log_path=log_path,
7697
7709
  include_prereleases=include_prereleases,
7698
7710
  target_version=latest,
7711
+ allow_python_migration=False,
7712
+ on_migration_deferred=lambda: deferred.append(True),
7713
+ on_unverified_install=lambda: unverified.append(True),
7699
7714
  )
7700
7715
  if success:
7701
7716
  self._update_available = (False, None)
7702
- # The install command is unpinned, so the installer may have
7703
- # resolved a release newer than the one the update check observed
7704
- # (a release published while the session sat open). Report what
7705
- # actually landed on disk rather than the stale check result.
7717
+ # uv installs are pinned and verified, and other methods report
7718
+ # the target, so this matches `latest`; the fallback is defensive.
7706
7719
  upgraded_to = installed or latest
7707
7720
  # uv may have installed the upgraded shim into a directory that
7708
7721
  # isn't first on the user's PATH (e.g. a leftover pre-uv
@@ -7729,6 +7742,24 @@ class DeepAgentsApp(App):
7729
7742
  await self._mount_message(
7730
7743
  ErrorMessage(format_shadowed_dcode_warning(shadow)),
7731
7744
  )
7745
+ elif unverified:
7746
+ # This session lazily imports modules that uv may have replaced.
7747
+ await self._mount_message(
7748
+ ErrorMessage(
7749
+ "Update could not be verified; see above. Quit and relaunch "
7750
+ "dcode before continuing."
7751
+ ),
7752
+ )
7753
+ elif deferred:
7754
+ # Nothing ran, so there is no failure or log to point at.
7755
+ await self._mount_message(AppMessage("Update deferred; see above."))
7756
+ elif notices:
7757
+ # The notice already carries its own recovery steps; a generic
7758
+ # command here would contradict them.
7759
+ log_line = f"\nLog: {log_path}" if log_path is not None else ""
7760
+ await self._mount_message(
7761
+ AppMessage(f"Update failed; see the message above.{log_line}"),
7762
+ )
7732
7763
  else:
7733
7764
  cmd = upgrade_command(
7734
7765
  include_prereleases=upgrade_include_prereleases,
@@ -8850,7 +8881,7 @@ class DeepAgentsApp(App):
8850
8881
  # composes its own rack, and modals must not retune the main one.
8851
8882
  screen = chrome.screen
8852
8883
  rack = screen.get_child_by_id("textual-toastrack")
8853
- except (NoMatches, NoScreen):
8884
+ except NoMatches, NoScreen:
8854
8885
  return
8855
8886
  region = chrome.region
8856
8887
  if not region:
@@ -10826,7 +10857,7 @@ class DeepAgentsApp(App):
10826
10857
  """
10827
10858
  try:
10828
10859
  messages = self.query_one("#messages", Container)
10829
- except (NoMatches, ScreenStackError):
10860
+ except NoMatches, ScreenStackError:
10830
10861
  logger.debug(
10831
10862
  "Messages container unavailable; skipping transient status %r",
10832
10863
  content,
@@ -11474,7 +11505,7 @@ class DeepAgentsApp(App):
11474
11505
  """
11475
11506
  try:
11476
11507
  await widget.remove()
11477
- except (AttributeError, RuntimeError):
11508
+ except AttributeError, RuntimeError:
11478
11509
  logger.debug(
11479
11510
  "Failed to remove %s widget during %s",
11480
11511
  prompt_name,
@@ -14027,7 +14058,7 @@ class DeepAgentsApp(App):
14027
14058
  proc.kill()
14028
14059
  with suppress(ProcessLookupError, OSError):
14029
14060
  await proc.wait()
14030
- except (ProcessLookupError, OSError):
14061
+ except ProcessLookupError, OSError:
14031
14062
  pass
14032
14063
 
14033
14064
  async def _open_url_command(self, command: str, cmd: str) -> None:
@@ -18253,7 +18284,7 @@ class DeepAgentsApp(App):
18253
18284
 
18254
18285
  clear_caches()
18255
18286
  self._sync_status_model()
18256
- except (OSError, ValueError):
18287
+ except OSError, ValueError:
18257
18288
  logger.exception("Failed to reload configuration")
18258
18289
  await self._mount_message(
18259
18290
  AppMessage(
@@ -18614,7 +18645,9 @@ class DeepAgentsApp(App):
18614
18645
  _, conversation = await self._get_context_usage_counts()
18615
18646
  return conversation
18616
18647
 
18617
- async def _handle_server_offload(self, config: RunnableConfig) -> None:
18648
+ async def _handle_server_offload(
18649
+ self, config: RunnableConfig, *, automatic: bool = False
18650
+ ) -> None:
18618
18651
  """Request and render the built-in server-owned offload operation."""
18619
18652
  from deepagents_code.hooks.client_lifecycle import ClientHookStopError
18620
18653
 
@@ -18641,6 +18674,8 @@ class DeepAgentsApp(App):
18641
18674
  await self._sync_session_cost_from_checkpoint()
18642
18675
 
18643
18676
  status = result.get("status")
18677
+ if automatic and status in {"empty", "noop"}:
18678
+ return
18644
18679
  if status == "empty":
18645
18680
  await self._mount_message(
18646
18681
  AppMessage("Nothing to offload — start a conversation first")
@@ -18829,7 +18864,7 @@ class DeepAgentsApp(App):
18829
18864
  "Compacting idle context before cache expiry.",
18830
18865
  markup=False,
18831
18866
  )
18832
- await self._handle_offload(reserved=True)
18867
+ await self._handle_offload(reserved=True, automatic=idle_window is not None)
18833
18868
  finally:
18834
18869
  self._offload_task_started = False
18835
18870
  self._offload_worker = None
@@ -18837,16 +18872,19 @@ class DeepAgentsApp(App):
18837
18872
  if not self._startup_sequence_running:
18838
18873
  await self._process_next_from_queue()
18839
18874
 
18840
- async def _handle_offload(self, *, reserved: bool = False) -> None:
18875
+ async def _handle_offload(
18876
+ self, *, reserved: bool = False, automatic: bool = False
18877
+ ) -> None:
18841
18878
  """Run `/offload`, always releasing its busy-state reservation.
18842
18879
 
18843
18880
  Args:
18844
18881
  reserved: Whether the caller already reserved the turn synchronously.
18882
+ automatic: Suppress no-op feedback for idle cache maintenance.
18845
18883
  """
18846
18884
  if not reserved:
18847
18885
  self._set_agent_running(True)
18848
18886
  try:
18849
- await self._offload_impl()
18887
+ await self._offload_impl(automatic=automatic)
18850
18888
  finally:
18851
18889
  self._set_agent_running(False)
18852
18890
  try:
@@ -18854,7 +18892,7 @@ class DeepAgentsApp(App):
18854
18892
  except Exception: # best-effort spinner cleanup
18855
18893
  logger.exception("Failed to dismiss spinner after offload")
18856
18894
 
18857
- async def _offload_impl(self) -> None:
18895
+ async def _offload_impl(self, *, automatic: bool = False) -> None:
18858
18896
  """Offload older messages through the dcode server operation."""
18859
18897
  if not self._agent or not self._lc_thread_id:
18860
18898
  await self._mount_message(
@@ -18870,7 +18908,7 @@ class DeepAgentsApp(App):
18870
18908
  return
18871
18909
 
18872
18910
  config: RunnableConfig = {"configurable": {"thread_id": self._lc_thread_id}}
18873
- await self._handle_server_offload(config)
18911
+ await self._handle_server_offload(config, automatic=automatic)
18874
18912
 
18875
18913
  def _start_plugin_auto_update(self) -> None:
18876
18914
  """Start the plugin auto-update worker."""
@@ -20985,7 +21023,7 @@ class DeepAgentsApp(App):
20985
21023
  try:
20986
21024
  resumable = await thread_exists(previous_thread_id)
20987
21025
  owner = await get_thread_agent(previous_thread_id) if resumable else None
20988
- except (sqlite3.Error, OSError):
21026
+ except sqlite3.Error, OSError:
20989
21027
  # Keep store failures visible because they are not routine suppression.
20990
21028
  logger.info(
20991
21029
  "Could not check whether previous thread %s is resumable",
@@ -22465,18 +22503,18 @@ class DeepAgentsApp(App):
22465
22503
  if self._pending_approval_widget:
22466
22504
  try:
22467
22505
  self._pending_approval_widget.action_select_reject()
22468
- except (AttributeError, RuntimeError):
22506
+ except AttributeError, RuntimeError:
22469
22507
  logger.exception("force-clear: failed to reject pending approval")
22470
22508
  if self._pending_ask_user_widget:
22471
22509
  try:
22472
22510
  self._pending_ask_user_widget.action_cancel()
22473
- except (AttributeError, RuntimeError):
22511
+ except AttributeError, RuntimeError:
22474
22512
  logger.exception("force-clear: failed to cancel pending ask-user")
22475
22513
  self._cancel_goal_proposal_worker()
22476
22514
  if self._pending_goal_review_widget:
22477
22515
  try:
22478
22516
  self._pending_goal_review_widget.action_cancel()
22479
- except (AttributeError, RuntimeError):
22517
+ except AttributeError, RuntimeError:
22480
22518
  logger.exception("force-clear: failed to cancel pending goal review")
22481
22519
  if self._shell_running and self._shell_worker:
22482
22520
  self._shell_worker.cancel()
@@ -23476,14 +23514,14 @@ class DeepAgentsApp(App):
23476
23514
  asyncio.shield(worker.wait()),
23477
23515
  timeout=_GRACEFUL_EXIT_WAIT_SECONDS,
23478
23516
  )
23479
- except (asyncio.CancelledError, WorkerCancelled):
23517
+ except asyncio.CancelledError, WorkerCancelled:
23480
23518
  # Expected: exit() cancelled the worker above, so
23481
23519
  # its cancellation handler ran to completion.
23482
23520
  logger.debug(
23483
23521
  "Agent worker cancelled cleanly before app exit",
23484
23522
  exc_info=True,
23485
23523
  )
23486
- except (TimeoutError, WorkerFailed):
23524
+ except TimeoutError, WorkerFailed:
23487
23525
  # The worker did not finish within the window, so
23488
23526
  # the in-flight run's server-side trace may be
23489
23527
  # incomplete. Surface above debug so the loss isn't
@@ -26025,7 +26063,7 @@ class DeepAgentsApp(App):
26025
26063
  if self._pending_ask_user_widget is not None:
26026
26064
  try:
26027
26065
  self._pending_ask_user_widget.action_cancel()
26028
- except (AttributeError, RuntimeError):
26066
+ except AttributeError, RuntimeError:
26029
26067
  logger.debug(
26030
26068
  "Failed to cancel pending ask-user during agent swap",
26031
26069
  exc_info=True,
@@ -26424,7 +26462,7 @@ class DeepAgentsApp(App):
26424
26462
  cli_version="0.1.0",
26425
26463
  release_age=" (released 2 days ago)",
26426
26464
  installed_age="",
26427
- upgrade_cmd="uv tool upgrade deepagents-code",
26465
+ upgrade_cmd="uv tool install -U deepagents-code",
26428
26466
  )
26429
26467
  self._notice_registry.add(update_notification)
26430
26468
  self._update_modal_pending.set()
@@ -27261,10 +27299,25 @@ class DeepAgentsApp(App):
27261
27299
  show_toast=not progress_modal_visible,
27262
27300
  )
27263
27301
  return
27302
+
27303
+ notices: list[str] = []
27304
+ deferred: list[bool] = []
27305
+ unverified: list[bool] = []
27306
+
27307
+ def show_upgrade_notice(message: str) -> None:
27308
+ notices.append(message)
27309
+ screen.append_line(message)
27310
+ self.notify(message, timeout=15, markup=False)
27311
+
27312
+ # Same live-session constraint as `/update`.
27264
27313
  success, output, installed = await perform_upgrade(
27265
27314
  progress=screen.append_line,
27315
+ on_notice=show_upgrade_notice,
27266
27316
  log_path=log_path,
27267
27317
  target_version=payload.latest,
27318
+ allow_python_migration=False,
27319
+ on_migration_deferred=lambda: deferred.append(True),
27320
+ on_unverified_install=lambda: unverified.append(True),
27268
27321
  )
27269
27322
  if success:
27270
27323
  self._notice_registry.remove(entry.key)
@@ -27289,11 +27342,9 @@ class DeepAgentsApp(App):
27289
27342
  markup=False,
27290
27343
  )
27291
27344
  return
27292
- # The install command is unpinned, so the installer
27293
- # may have resolved a release newer than the one this
27294
- # notification advertised (a release published while
27295
- # the notice sat open). Report what actually landed on
27296
- # disk rather than the stale payload version.
27345
+ # uv installs are pinned and verified, and other
27346
+ # methods report the target, so this matches the
27347
+ # payload; the fallback is defensive.
27297
27348
  upgraded_to = installed or payload.latest
27298
27349
  # After `mark_success`: the modal stays un-dismissable
27299
27350
  # until it is marked done, and this worker runs with
@@ -27311,12 +27362,35 @@ class DeepAgentsApp(App):
27311
27362
  markup=False,
27312
27363
  )
27313
27364
  return
27365
+ self._notice_registry.remove(entry.key)
27366
+ if deferred:
27367
+ logger.info(
27368
+ "Deferred the Python migration for v%s", payload.latest
27369
+ )
27370
+ screen.mark_warning("Update deferred. See details.")
27371
+ return
27314
27372
  logger.warning(
27315
27373
  "Auto-upgrade failed for v%s. Output:\n%s",
27316
27374
  payload.latest,
27317
27375
  output,
27318
27376
  )
27319
- self._notice_registry.remove(entry.key)
27377
+ if unverified:
27378
+ # This session lazily imports modules that uv may have
27379
+ # replaced.
27380
+ restart = (
27381
+ "Update could not be verified. Quit and relaunch dcode "
27382
+ "before continuing."
27383
+ )
27384
+ screen.mark_warning(restart)
27385
+ self.notify(
27386
+ restart, severity="warning", timeout=20, markup=False
27387
+ )
27388
+ return
27389
+ if notices:
27390
+ # The notice toast and modal line already carry the
27391
+ # notice's own recovery steps.
27392
+ screen.mark_warning("Update did not complete. See details.")
27393
+ return
27320
27394
  screen.mark_failure(cmd)
27321
27395
  snippet = _truncate(output, limit=160) if output else ""
27322
27396
  message = f"Auto-update failed. Run manually: {cmd}"
@@ -27859,7 +27933,7 @@ class DeepAgentsApp(App):
27859
27933
  """
27860
27934
  try:
27861
27935
  banner = self.query_one("#welcome-banner", WelcomeBanner)
27862
- except (NoMatches, ScreenStackError):
27936
+ except NoMatches, ScreenStackError:
27863
27937
  logger.debug("Welcome banner not mounted during MCP count refresh")
27864
27938
  return
27865
27939
  banner.set_connected(
@@ -30121,7 +30195,7 @@ class DeepAgentsApp(App):
30121
30195
 
30122
30196
  msg = f"No previous threads for '{active_agent}' to resume."
30123
30197
  await self._mount_message(AppMessage(msg))
30124
- except (sqlite3.Error, OSError):
30198
+ except sqlite3.Error, OSError:
30125
30199
  # Expected thread-store failures: log for a debug session and tell
30126
30200
  # the user to retry. Mirrors the launch-time resolver's handling.
30127
30201
  logger.warning(
@@ -30963,7 +31037,7 @@ class DeepAgentsApp(App):
30963
31037
  credentials.preview_reload_from_environment,
30964
31038
  start_path=cwd,
30965
31039
  )
30966
- except (OSError, ValueError):
31040
+ except OSError, ValueError:
30967
31041
  # Environmental failures (unreadable dotenv, malformed values) are
30968
31042
  # expected and non-fatal for a best-effort preview. Programming
30969
31043
  # errors (KeyError/TypeError/ImportError) are left to propagate so a
@@ -32183,7 +32257,7 @@ class DeepAgentsApp(App):
32183
32257
  self._retry_status_widget = None
32184
32258
  try:
32185
32259
  messages = self.query_one("#messages", Container)
32186
- except (NoMatches, ScreenStackError):
32260
+ except NoMatches, ScreenStackError:
32187
32261
  messages = None
32188
32262
  if messages is not None and messages.is_attached:
32189
32263
  new_widget = AppMessage(
@@ -408,7 +408,7 @@ def _load_approval_state(path: Path) -> dict[str, object]:
408
408
  data = json.loads(path.read_text(encoding="utf-8"))
409
409
  except FileNotFoundError:
410
410
  return {}
411
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
411
+ except OSError, UnicodeDecodeError, json.JSONDecodeError:
412
412
  logger.warning(
413
413
  "Ignoring unreadable or corrupt approval state at %s; callers fail "
414
414
  "closed and the file will be overwritten on the next save",
@@ -548,7 +548,7 @@ def _validate_temp_artifact(value: object) -> AutoTempArtifact | None:
548
548
  return None
549
549
  try:
550
550
  file_path = Path(cast("str", raw_file_path))
551
- except (OSError, TypeError, ValueError):
551
+ except OSError, TypeError, ValueError:
552
552
  return None
553
553
  if not file_path.is_absolute() or not file_path.name.startswith(
554
554
  _TEMP_ARTIFACT_PREFIX
@@ -794,7 +794,7 @@ def _known_credential_values(
794
794
  and len(value) >= _MIN_SECRET_LENGTH
795
795
  ):
796
796
  values.add(value)
797
- except (OSError, RuntimeError, TypeError, ValueError):
797
+ except OSError, RuntimeError, TypeError, ValueError:
798
798
  logger.debug("Could not load stored credential values for Auto redaction")
799
799
  return tuple(sorted(values, key=len, reverse=True))
800
800
 
@@ -2033,7 +2033,7 @@ def _resolve_path(root: Path, raw: object) -> Path | None:
2033
2033
  if not candidate.is_absolute():
2034
2034
  candidate = root / candidate
2035
2035
  return candidate.resolve(strict=False)
2036
- except (OSError, RuntimeError, ValueError):
2036
+ except OSError, RuntimeError, ValueError:
2037
2037
  return None
2038
2038
 
2039
2039
 
@@ -2662,7 +2662,7 @@ class AutoModeHITLMiddleware(HumanInTheLoopMiddleware[AutoModeState, Any, Any]):
2662
2662
  if not targets_managed_artifact:
2663
2663
  try:
2664
2664
  candidate_stat = candidate.stat()
2665
- except (OSError, ValueError):
2665
+ except OSError, ValueError:
2666
2666
  pass
2667
2667
  else:
2668
2668
  targets_managed_artifact = any(
deepagents_code/btw.py CHANGED
@@ -186,7 +186,7 @@ async def _open_text_stream(
186
186
  messages: list[BaseMessage],
187
187
  config: RunnableConfig,
188
188
  settings: Mapping[str, Any],
189
- ) -> tuple[AsyncGenerator[AIMessageChunk, None], str]:
189
+ ) -> tuple[AsyncGenerator[AIMessageChunk], str]:
190
190
  """Open one attempt through its first visible fragment, closing failures.
191
191
 
192
192
  Returns:
@@ -194,7 +194,7 @@ async def _open_text_stream(
194
194
  """
195
195
  # BaseChatModel.astream is an async generator, annotated as an iterator.
196
196
  stream = cast(
197
- "AsyncGenerator[AIMessageChunk, None]",
197
+ "AsyncGenerator[AIMessageChunk]",
198
198
  model.astream(messages, config=config, **settings),
199
199
  )
200
200
  try:
@@ -133,7 +133,7 @@ class _BtwStreamingResponse(Response):
133
133
  await emit("error", {"detail": "Side question timed out. Try again."})
134
134
  except ClientDisconnect:
135
135
  raise # A closed transport cannot receive an error event.
136
- except (Exception, SystemExit):
136
+ except Exception, SystemExit:
137
137
  logger.exception("Side question failed")
138
138
  await emit(
139
139
  "error",
@@ -230,7 +230,7 @@ async def btw(request: Request) -> Response:
230
230
  return JSONResponse(
231
231
  {"detail": "Side question timed out. Try again."}, status_code=504
232
232
  )
233
- except (Exception, SystemExit):
233
+ except Exception, SystemExit:
234
234
  logger.exception("Side question failed")
235
235
  return JSONResponse(
236
236
  {"detail": "Side question failed on the server; see the server log."},