@researai/deepscientist 1.5.0 → 1.5.1

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 (163) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +19 -179
  3. package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
  4. package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
  5. package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
  6. package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
  7. package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
  8. package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
  9. package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
  10. package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
  11. package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
  12. package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
  13. package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
  14. package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
  15. package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
  16. package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
  17. package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
  18. package/bin/ds.js +233 -53
  19. package/docs/en/00_QUICK_START.md +134 -0
  20. package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
  21. package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
  22. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
  23. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
  24. package/docs/en/05_TUI_GUIDE.md +141 -0
  25. package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
  26. package/docs/en/07_MEMORY_AND_MCP.md +253 -0
  27. package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
  28. package/docs/en/09_DOCTOR.md +108 -0
  29. package/docs/en/90_ARCHITECTURE.md +245 -0
  30. package/docs/en/91_DEVELOPMENT.md +195 -0
  31. package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
  32. package/docs/zh/00_QUICK_START.md +134 -0
  33. package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
  34. package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
  35. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
  36. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
  37. package/docs/zh/05_TUI_GUIDE.md +128 -0
  38. package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
  39. package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
  40. package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
  41. package/docs/zh/09_DOCTOR.md +112 -0
  42. package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
  43. package/install.sh +32 -8
  44. package/package.json +4 -2
  45. package/pyproject.toml +1 -1
  46. package/src/deepscientist/artifact/guidance.py +9 -2
  47. package/src/deepscientist/artifact/service.py +482 -22
  48. package/src/deepscientist/bash_exec/monitor.py +27 -5
  49. package/src/deepscientist/bash_exec/runtime.py +639 -0
  50. package/src/deepscientist/bash_exec/service.py +99 -16
  51. package/src/deepscientist/bridges/base.py +3 -0
  52. package/src/deepscientist/bridges/connectors.py +292 -13
  53. package/src/deepscientist/channels/qq.py +19 -2
  54. package/src/deepscientist/channels/relay.py +1 -0
  55. package/src/deepscientist/cli.py +32 -25
  56. package/src/deepscientist/config/models.py +28 -2
  57. package/src/deepscientist/config/service.py +201 -6
  58. package/src/deepscientist/connector_runtime.py +2 -0
  59. package/src/deepscientist/daemon/api/handlers.py +50 -5
  60. package/src/deepscientist/daemon/api/router.py +1 -0
  61. package/src/deepscientist/daemon/app.py +442 -15
  62. package/src/deepscientist/doctor.py +444 -0
  63. package/src/deepscientist/home.py +1 -0
  64. package/src/deepscientist/latex_runtime.py +17 -4
  65. package/src/deepscientist/lingzhu_support.py +182 -0
  66. package/src/deepscientist/mcp/server.py +49 -2
  67. package/src/deepscientist/prompts/builder.py +181 -58
  68. package/src/deepscientist/quest/layout.py +1 -0
  69. package/src/deepscientist/quest/service.py +63 -2
  70. package/src/deepscientist/quest/stage_views.py +19 -1
  71. package/src/deepscientist/runtime_tools/__init__.py +16 -0
  72. package/src/deepscientist/runtime_tools/builtins.py +19 -0
  73. package/src/deepscientist/runtime_tools/models.py +29 -0
  74. package/src/deepscientist/runtime_tools/registry.py +40 -0
  75. package/src/deepscientist/runtime_tools/service.py +59 -0
  76. package/src/deepscientist/runtime_tools/tinytex.py +25 -0
  77. package/src/deepscientist/tinytex.py +276 -0
  78. package/src/prompts/connectors/lingzhu.md +12 -0
  79. package/src/prompts/connectors/qq.md +121 -0
  80. package/src/prompts/system.md +177 -33
  81. package/src/skills/analysis-campaign/SKILL.md +22 -6
  82. package/src/skills/baseline/SKILL.md +5 -4
  83. package/src/skills/decision/SKILL.md +4 -3
  84. package/src/skills/experiment/SKILL.md +5 -4
  85. package/src/skills/finalize/SKILL.md +5 -4
  86. package/src/skills/idea/SKILL.md +5 -4
  87. package/src/skills/intake-audit/SKILL.md +277 -0
  88. package/src/skills/intake-audit/references/state-audit-template.md +41 -0
  89. package/src/skills/rebuttal/SKILL.md +407 -0
  90. package/src/skills/rebuttal/references/action-plan-template.md +63 -0
  91. package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
  92. package/src/skills/rebuttal/references/response-letter-template.md +113 -0
  93. package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
  94. package/src/skills/review/SKILL.md +293 -0
  95. package/src/skills/review/references/experiment-todo-template.md +29 -0
  96. package/src/skills/review/references/review-report-template.md +83 -0
  97. package/src/skills/review/references/revision-log-template.md +40 -0
  98. package/src/skills/scout/SKILL.md +5 -4
  99. package/src/skills/write/SKILL.md +7 -3
  100. package/src/tui/dist/components/WelcomePanel.js +17 -43
  101. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
  102. package/src/tui/package.json +1 -1
  103. package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
  104. package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
  105. package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
  106. package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
  107. package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
  108. package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
  109. package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
  110. package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
  111. package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
  112. package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
  113. package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
  114. package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
  115. package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
  116. package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
  117. package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
  118. package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
  119. package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
  120. package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
  121. package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
  122. package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
  123. package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
  124. package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
  125. package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
  126. package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
  127. package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
  128. package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
  129. package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
  130. package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
  131. package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
  132. package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
  133. package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
  134. package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
  135. package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
  136. package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
  137. package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
  138. package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
  139. package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
  140. package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
  141. package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
  142. package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
  143. package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
  144. package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
  145. package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
  146. package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
  147. package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
  148. package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
  149. package/src/ui/dist/index.html +2 -2
  150. package/assets/fonts/Inter-Variable.ttf +0 -0
  151. package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
  152. package/assets/fonts/NunitoSans-Variable.ttf +0 -0
  153. package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
  154. package/assets/fonts/SourceSans3-Variable.ttf +0 -0
  155. package/assets/fonts/ds-fonts.css +0 -83
  156. package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
  157. package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
  158. package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
  159. package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
  160. package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
  161. package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
  162. package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
  163. package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
@@ -1778,7 +1778,13 @@ class QuestService:
1778
1778
  )
1779
1779
  return documents
1780
1780
 
1781
- def explorer(self, quest_id: str, revision: str | None = None, mode: str | None = None) -> dict:
1781
+ def explorer(
1782
+ self,
1783
+ quest_id: str,
1784
+ revision: str | None = None,
1785
+ mode: str | None = None,
1786
+ profile: str | None = None,
1787
+ ) -> dict:
1782
1788
  if revision:
1783
1789
  return self._revision_explorer(quest_id, revision=revision, mode=mode or "ref")
1784
1790
 
@@ -1796,6 +1802,7 @@ class QuestService:
1796
1802
  workspace_root,
1797
1803
  git_status=git_status,
1798
1804
  changed_paths=changed_paths,
1805
+ profile=profile,
1799
1806
  )
1800
1807
  sections = self._group_explorer_sections(root_nodes)
1801
1808
 
@@ -1807,6 +1814,7 @@ class QuestService:
1807
1814
  "revision": None,
1808
1815
  "label": "Latest",
1809
1816
  "read_only": False,
1817
+ "profile": profile,
1810
1818
  },
1811
1819
  "sections": sections,
1812
1820
  }
@@ -2626,6 +2634,8 @@ class QuestService:
2626
2634
  message: str,
2627
2635
  response_phase: str | None = None,
2628
2636
  reply_mode: str | None = None,
2637
+ surface_actions: list[dict[str, Any]] | None = None,
2638
+ connector_hints: dict[str, Any] | None = None,
2629
2639
  created_at: str | None = None,
2630
2640
  ) -> dict[str, Any]:
2631
2641
  timestamp = created_at or utc_now()
@@ -2639,6 +2649,8 @@ class QuestService:
2639
2649
  "message": message,
2640
2650
  "response_phase": response_phase,
2641
2651
  "reply_mode": reply_mode,
2652
+ "surface_actions": [dict(item) for item in (surface_actions or []) if isinstance(item, dict)],
2653
+ "connector_hints": dict(connector_hints) if isinstance(connector_hints, dict) else {},
2642
2654
  "created_at": timestamp,
2643
2655
  }
2644
2656
  append_jsonl(self._interaction_journal_path(quest_root), payload)
@@ -3025,6 +3037,8 @@ class QuestService:
3025
3037
  *,
3026
3038
  git_status: dict[str, str],
3027
3039
  changed_paths: dict[str, dict],
3040
+ profile: str | None = None,
3041
+ depth: int = 0,
3028
3042
  ) -> list[dict]:
3029
3043
  if not root.exists():
3030
3044
  return []
@@ -3044,6 +3058,9 @@ class QuestService:
3044
3058
  try:
3045
3059
  if self._skip_explorer_path(quest_root, path):
3046
3060
  continue
3061
+ relative = path.relative_to(quest_root).as_posix()
3062
+ if self._skip_explorer_profile_relative(relative, profile):
3063
+ continue
3047
3064
  except OSError:
3048
3065
  continue
3049
3066
  try:
@@ -3051,7 +3068,19 @@ class QuestService:
3051
3068
  except OSError:
3052
3069
  continue
3053
3070
  if is_dir:
3054
- children = self._tree_children(quest_root, path, git_status=git_status, changed_paths=changed_paths)
3071
+ truncate_children = self._truncate_explorer_directory(relative, profile=profile, depth=depth)
3072
+ children = (
3073
+ []
3074
+ if truncate_children
3075
+ else self._tree_children(
3076
+ quest_root,
3077
+ path,
3078
+ git_status=git_status,
3079
+ changed_paths=changed_paths,
3080
+ profile=profile,
3081
+ depth=depth + 1,
3082
+ )
3083
+ )
3055
3084
  nodes.append(
3056
3085
  self._directory_node(
3057
3086
  quest_root,
@@ -3143,6 +3172,38 @@ class QuestService:
3143
3172
  parts = PurePosixPath(relative).parts
3144
3173
  return "__pycache__" in parts or ".pytest_cache" in parts
3145
3174
 
3175
+ @staticmethod
3176
+ def _skip_explorer_profile_relative(relative: str, profile: str | None) -> bool:
3177
+ if profile != "mobile":
3178
+ return False
3179
+ normalized = relative.strip("/")
3180
+ if not normalized:
3181
+ return False
3182
+ parts = PurePosixPath(normalized).parts
3183
+ top = parts[0] if parts else normalized
3184
+ if top in {".codex", ".claude", ".ds", "tmp", "userfiles", "artifacts"}:
3185
+ return True
3186
+ if top.startswith(".") and normalized not in {".gitignore"}:
3187
+ return True
3188
+ return False
3189
+
3190
+ @staticmethod
3191
+ def _truncate_explorer_directory(relative: str, *, profile: str | None, depth: int) -> bool:
3192
+ if profile != "mobile":
3193
+ return False
3194
+ normalized = relative.strip("/")
3195
+ if not normalized:
3196
+ return False
3197
+ parts = PurePosixPath(normalized).parts
3198
+ top = parts[0] if parts else normalized
3199
+ if top == "memory":
3200
+ return False
3201
+ if top == "baselines":
3202
+ return depth >= 1
3203
+ if top in {"literature", "paper", "experiments", "handoffs"}:
3204
+ return depth >= 2
3205
+ return depth >= 1
3206
+
3146
3207
  @staticmethod
3147
3208
  def _classify_path_scope(quest_root: Path, path: Path) -> tuple[str, bool]:
3148
3209
  relative = path.relative_to(quest_root).as_posix()
@@ -1093,7 +1093,8 @@ class QuestStageViewBuilder:
1093
1093
  latest = stage_items[-1] if stage_items else None
1094
1094
  latest_payload = self._payload(latest or {})
1095
1095
  slices = [dict(item) for item in (manifest.get("slices") or []) if isinstance(item, dict)]
1096
- todo_items = [
1096
+ manifest_todo_items = [dict(item) for item in (manifest.get("todo_items") or []) if isinstance(item, dict)]
1097
+ todo_items = manifest_todo_items or [
1097
1098
  {
1098
1099
  "todo_id": str(item.get("slice_id") or f"slice-{index}"),
1099
1100
  "slice_id": str(item.get("slice_id") or f"slice-{index}"),
@@ -1102,6 +1103,11 @@ class QuestStageViewBuilder:
1102
1103
  "research_question": item.get("research_question"),
1103
1104
  "experimental_design": item.get("experimental_design"),
1104
1105
  "completion_condition": item.get("must_not_simplify") or item.get("goal") or "Complete the planned analysis slice.",
1106
+ "why_now": item.get("why_now"),
1107
+ "success_criteria": item.get("success_criteria"),
1108
+ "abandonment_criteria": item.get("abandonment_criteria"),
1109
+ "reviewer_item_ids": item.get("reviewer_item_ids") or [],
1110
+ "manuscript_targets": item.get("manuscript_targets") or [],
1105
1111
  }
1106
1112
  for index, item in enumerate(slices, start=1)
1107
1113
  ]
@@ -1124,8 +1130,17 @@ class QuestStageViewBuilder:
1124
1130
  "run_kind": item.get("run_kind"),
1125
1131
  "question": item.get("research_question") or detail_payload.get("question"),
1126
1132
  "hypothesis": item.get("hypothesis"),
1133
+ "why_now": item.get("why_now"),
1134
+ "success_criteria": item.get("success_criteria"),
1135
+ "abandonment_criteria": item.get("abandonment_criteria"),
1136
+ "reviewer_item_ids": item.get("reviewer_item_ids") or [],
1137
+ "manuscript_targets": item.get("manuscript_targets") or [],
1127
1138
  "status": item.get("status") or run_payload.get("status") or "pending",
1128
1139
  "metric_summary": run_payload.get("metrics_summary") or {},
1140
+ "claim_impact": detail_payload.get("claim_impact"),
1141
+ "reviewer_resolution": detail_payload.get("reviewer_resolution"),
1142
+ "manuscript_update_hint": detail_payload.get("manuscript_update_hint"),
1143
+ "next_recommendation": detail_payload.get("next_recommendation"),
1129
1144
  "deviations": detail_payload.get("deviations") or [],
1130
1145
  "evidence_paths": detail_payload.get("evidence_paths") or [],
1131
1146
  "plan_path": item.get("plan_path"),
@@ -1189,11 +1204,14 @@ class QuestStageViewBuilder:
1189
1204
  "goal": manifest.get("goal"),
1190
1205
  "parent_run_id": manifest.get("parent_run_id"),
1191
1206
  "parent_branch": manifest.get("parent_branch") or self.branch_name,
1207
+ "campaign_origin": manifest.get("campaign_origin"),
1192
1208
  "selected_outline_ref": manifest.get("selected_outline_ref"),
1193
1209
  "todo_items": todo_items,
1194
1210
  "slices": slice_rows,
1195
1211
  "summary": latest_payload.get("summary"),
1196
1212
  "manifest_path": manifest.get("_manifest_path"),
1213
+ "charter_path": manifest.get("charter_path"),
1214
+ "todo_manifest_path": manifest.get("todo_manifest_path"),
1197
1215
  }
1198
1216
  },
1199
1217
  )
@@ -0,0 +1,16 @@
1
+ from .builtins import register_builtin_runtime_tools
2
+ from .models import RuntimeBinaryMatch, RuntimeTool, RuntimeToolFactory, RuntimeToolStatus
3
+ from .registry import get_runtime_tool_factory, list_runtime_tool_names, register_runtime_tool
4
+ from .service import RuntimeToolService
5
+
6
+ __all__ = [
7
+ "RuntimeBinaryMatch",
8
+ "RuntimeTool",
9
+ "RuntimeToolFactory",
10
+ "RuntimeToolService",
11
+ "RuntimeToolStatus",
12
+ "get_runtime_tool_factory",
13
+ "list_runtime_tool_names",
14
+ "register_builtin_runtime_tools",
15
+ "register_runtime_tool",
16
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .registry import register_runtime_tool
6
+ from .tinytex import TinyTeXRuntimeTool
7
+
8
+
9
+ def register_builtin_runtime_tools(*, home=None) -> None:
10
+ def _tinytex_factory(**kwargs):
11
+ selected_home = kwargs.get("home") or home
12
+ if selected_home is None:
13
+ raise ValueError("Runtime tool factories require `home`.")
14
+ return TinyTeXRuntimeTool(Path(selected_home))
15
+
16
+ register_runtime_tool("tinytex", _tinytex_factory)
17
+
18
+
19
+ __all__ = ["register_builtin_runtime_tools"]
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, Protocol
5
+
6
+
7
+ class RuntimeTool(Protocol):
8
+ tool_name: str
9
+
10
+ def status(self) -> dict[str, Any]:
11
+ ...
12
+
13
+ def install(self) -> dict[str, Any]:
14
+ ...
15
+
16
+ def resolve_binary(self, binary: str) -> dict[str, Any]:
17
+ ...
18
+
19
+
20
+ RuntimeToolFactory = Callable[..., RuntimeTool]
21
+ RuntimeBinaryMatch = dict[str, Any]
22
+ RuntimeToolStatus = dict[str, Any]
23
+
24
+ __all__ = [
25
+ "RuntimeBinaryMatch",
26
+ "RuntimeTool",
27
+ "RuntimeToolFactory",
28
+ "RuntimeToolStatus",
29
+ ]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from .models import RuntimeToolFactory
6
+
7
+
8
+ _RUNTIME_TOOL_FACTORIES: dict[str, RuntimeToolFactory] = {}
9
+ _RUNTIME_TOOL_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
10
+
11
+
12
+ def _normalize_runtime_tool_name(name: str) -> str:
13
+ normalized = str(name or "").strip().lower()
14
+ if not normalized or not _RUNTIME_TOOL_NAME_PATTERN.fullmatch(normalized):
15
+ raise ValueError("Runtime tool name must match `^[a-z0-9][a-z0-9_-]*$`.")
16
+ return normalized
17
+
18
+
19
+ def register_runtime_tool(name: str, factory: RuntimeToolFactory) -> None:
20
+ _RUNTIME_TOOL_FACTORIES[_normalize_runtime_tool_name(name)] = factory
21
+
22
+
23
+ def get_runtime_tool_factory(name: str) -> RuntimeToolFactory:
24
+ normalized = _normalize_runtime_tool_name(name)
25
+ try:
26
+ return _RUNTIME_TOOL_FACTORIES[normalized]
27
+ except KeyError as exc:
28
+ available = ", ".join(sorted(_RUNTIME_TOOL_FACTORIES)) or "none"
29
+ raise KeyError(f"Unknown runtime tool `{normalized}`. Available runtime tools: {available}.") from exc
30
+
31
+
32
+ def list_runtime_tool_names() -> list[str]:
33
+ return sorted(_RUNTIME_TOOL_FACTORIES)
34
+
35
+
36
+ __all__ = [
37
+ "get_runtime_tool_factory",
38
+ "list_runtime_tool_names",
39
+ "register_runtime_tool",
40
+ ]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Iterable
5
+
6
+ from ..shared import which
7
+ from .builtins import register_builtin_runtime_tools
8
+ from .models import RuntimeBinaryMatch, RuntimeTool, RuntimeToolStatus
9
+ from .registry import get_runtime_tool_factory, list_runtime_tool_names
10
+
11
+
12
+ class RuntimeToolService:
13
+ def __init__(self, home: Path) -> None:
14
+ self.home = home
15
+ register_builtin_runtime_tools(home=home)
16
+
17
+ def get_tool(self, name: str) -> RuntimeTool:
18
+ return get_runtime_tool_factory(name)(home=self.home)
19
+
20
+ def list_tool_names(self) -> list[str]:
21
+ return list_runtime_tool_names()
22
+
23
+ def status(self, name: str) -> RuntimeToolStatus:
24
+ return self.get_tool(name).status()
25
+
26
+ def install(self, name: str) -> RuntimeToolStatus:
27
+ return self.get_tool(name).install()
28
+
29
+ def all_statuses(self) -> dict[str, RuntimeToolStatus]:
30
+ return {name: self.status(name) for name in self.list_tool_names()}
31
+
32
+ def resolve_binary(
33
+ self,
34
+ binary: str,
35
+ *,
36
+ preferred_tools: Iterable[str] | None = None,
37
+ allow_system_fallback: bool = True,
38
+ ) -> RuntimeBinaryMatch:
39
+ normalized = str(binary or "").strip()
40
+ if not normalized:
41
+ return {"binary": None, "path": None, "source": None, "root": None, "bin_dir": None}
42
+
43
+ names = list(preferred_tools or self.list_tool_names())
44
+ for name in names:
45
+ match = self.get_tool(name).resolve_binary(normalized)
46
+ if isinstance(match, dict) and match.get("path"):
47
+ return match
48
+
49
+ system_path = which(normalized) if allow_system_fallback else None
50
+ return {
51
+ "binary": normalized,
52
+ "path": system_path,
53
+ "source": "path" if system_path else None,
54
+ "root": None,
55
+ "bin_dir": None,
56
+ }
57
+
58
+
59
+ __all__ = ["RuntimeToolService"]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from ..tinytex import inspect_latex_runtime, install_tinytex, resolve_tinytex_binary
7
+
8
+
9
+ class TinyTeXRuntimeTool:
10
+ tool_name = "tinytex"
11
+
12
+ def __init__(self, home: Path) -> None:
13
+ self.home = home
14
+
15
+ def status(self) -> dict[str, Any]:
16
+ return inspect_latex_runtime(self.home)
17
+
18
+ def install(self) -> dict[str, Any]:
19
+ return install_tinytex(self.home)
20
+
21
+ def resolve_binary(self, binary: str) -> dict[str, Any]:
22
+ return resolve_tinytex_binary(binary, self.home)
23
+
24
+
25
+ __all__ = ["TinyTeXRuntimeTool"]
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from urllib.error import URLError
10
+ from urllib.request import Request, urlopen
11
+
12
+ from .shared import which
13
+
14
+ _TINYTEX_DOC_URL = "https://yihui.org/tinytex/"
15
+ _TINYTEX_CHINESE_DOC_URL = "https://yihui.org/tinytex/cn/"
16
+ _TINYTEX_UNIX_INSTALLER_URL = "https://tinytex.yihui.org/install-bin-unix.sh"
17
+ _TINYTEX_WINDOWS_INSTALLER_URL = "https://tinytex.yihui.org/install-bin-windows.bat"
18
+
19
+
20
+ def _unique_paths(paths: list[Path]) -> list[Path]:
21
+ ordered: list[Path] = []
22
+ seen: set[str] = set()
23
+ for path in paths:
24
+ key = str(path.expanduser())
25
+ if key in seen:
26
+ continue
27
+ seen.add(key)
28
+ ordered.append(path.expanduser())
29
+ return ordered
30
+
31
+
32
+ def _home_tools_root(home: Path | None) -> Path | None:
33
+ if home is None:
34
+ return None
35
+ return home / "runtime" / "tools"
36
+
37
+
38
+ def tinytex_root_candidates(home: Path | None = None) -> list[Path]:
39
+ candidates: list[Path] = []
40
+ for env_name in ("DEEPSCIENTIST_TINYTEX_ROOT", "DS_TINYTEX_ROOT", "TINYTEX_DIR"):
41
+ raw = str(os.environ.get(env_name) or "").strip()
42
+ if raw:
43
+ candidates.append(Path(raw).expanduser())
44
+
45
+ tools_root = _home_tools_root(home)
46
+ if tools_root is not None:
47
+ candidates.append(tools_root / "TinyTeX")
48
+
49
+ if sys.platform.startswith("darwin"):
50
+ candidates.append(Path.home() / "Library" / "TinyTeX")
51
+ elif sys.platform.startswith("win"):
52
+ appdata_root = Path(os.environ.get("APPDATA") or (Path.home() / "AppData" / "Roaming"))
53
+ candidates.append(appdata_root / "TinyTeX")
54
+ else:
55
+ candidates.append(Path.home() / ".TinyTeX")
56
+
57
+ return _unique_paths(candidates)
58
+
59
+
60
+ def _binary_names(binary: str) -> list[str]:
61
+ normalized = str(binary or "").strip()
62
+ if not normalized:
63
+ return []
64
+ if sys.platform.startswith("win"):
65
+ return [f"{normalized}.exe", f"{normalized}.bat", f"{normalized}.cmd", normalized]
66
+ return [normalized]
67
+
68
+
69
+ def _tinytex_bin_dirs(root: Path) -> list[Path]:
70
+ bin_root = root / "bin"
71
+ if not bin_root.exists():
72
+ return []
73
+ child_dirs = sorted(path for path in bin_root.iterdir() if path.is_dir())
74
+ return child_dirs or ([bin_root] if bin_root.is_dir() else [])
75
+
76
+
77
+ def resolve_tinytex_binary(binary: str, home: Path | None = None) -> dict[str, Any]:
78
+ normalized = str(binary or "").strip()
79
+ if not normalized:
80
+ return {"binary": None, "path": None, "source": None, "root": None, "bin_dir": None}
81
+ for root in tinytex_root_candidates(home):
82
+ for bin_dir in _tinytex_bin_dirs(root):
83
+ for name in _binary_names(normalized):
84
+ candidate = bin_dir / name
85
+ if candidate.exists():
86
+ return {
87
+ "binary": normalized,
88
+ "path": str(candidate),
89
+ "source": "tinytex",
90
+ "root": str(root),
91
+ "bin_dir": str(bin_dir),
92
+ }
93
+ return {
94
+ "binary": normalized,
95
+ "path": None,
96
+ "source": None,
97
+ "root": None,
98
+ "bin_dir": None,
99
+ }
100
+
101
+
102
+ def resolve_latex_binary(binary: str, home: Path | None = None) -> dict[str, Any]:
103
+ tinytex_match = resolve_tinytex_binary(binary, home)
104
+ if tinytex_match.get("path"):
105
+ return tinytex_match
106
+ system_path = which(binary)
107
+ return {
108
+ "binary": str(binary or "").strip() or None,
109
+ "path": system_path,
110
+ "source": "path" if system_path else None,
111
+ "root": None,
112
+ "bin_dir": None,
113
+ }
114
+
115
+
116
+ def inspect_latex_runtime(home: Path | None = None) -> dict[str, Any]:
117
+ pdflatex = resolve_latex_binary("pdflatex", home)
118
+ xelatex = resolve_latex_binary("xelatex", home)
119
+ lualatex = resolve_latex_binary("lualatex", home)
120
+ bibtex = resolve_latex_binary("bibtex", home)
121
+ tinytex = resolve_tinytex_binary("pdflatex", home)
122
+
123
+ guidance: list[str] = []
124
+ warnings: list[str] = []
125
+ available = bool(pdflatex.get("path"))
126
+ bibtex_available = bool(bibtex.get("path"))
127
+
128
+ if not available:
129
+ warnings.append("Local PDF compilation is optional and currently unavailable because `pdflatex` is missing.")
130
+ guidance.append("Install a lightweight TinyTeX runtime with `ds latex install-runtime`.")
131
+ guidance.append(
132
+ "Or install a system LaTeX distribution that provides `pdflatex` and `bibtex`."
133
+ )
134
+ elif not bibtex_available:
135
+ warnings.append("`pdflatex` is available, but `bibtex` is missing. Bibliography builds may fail.")
136
+ guidance.append("Install TinyTeX with `ds latex install-runtime` or add `bibtex` to your system LaTeX distribution.")
137
+
138
+ summary = "A local `pdflatex` runtime is available for paper builds." if available else "Local `pdflatex` is not available."
139
+ if pdflatex.get("source") == "tinytex":
140
+ summary = "A TinyTeX-managed `pdflatex` runtime is available for paper builds."
141
+
142
+ return {
143
+ "ok": available,
144
+ "summary": summary,
145
+ "warnings": warnings,
146
+ "guidance": guidance,
147
+ "tinytex": {
148
+ "installed": bool(tinytex.get("path")),
149
+ "root": tinytex.get("root"),
150
+ "bin_dir": tinytex.get("bin_dir"),
151
+ "doc_url": _TINYTEX_DOC_URL,
152
+ "doc_url_zh": _TINYTEX_CHINESE_DOC_URL,
153
+ "installer_url": _TINYTEX_WINDOWS_INSTALLER_URL if sys.platform.startswith("win") else _TINYTEX_UNIX_INSTALLER_URL,
154
+ },
155
+ "binaries": {
156
+ "pdflatex": pdflatex,
157
+ "xelatex": xelatex,
158
+ "lualatex": lualatex,
159
+ "bibtex": bibtex,
160
+ },
161
+ }
162
+
163
+
164
+ def _download_installer(url: str) -> tuple[bool, str]:
165
+ request = Request(url, headers={"User-Agent": "DeepScientist TinyTeX bootstrap"})
166
+ try:
167
+ with urlopen(request, timeout=30) as response: # noqa: S310
168
+ payload = response.read()
169
+ except (OSError, TimeoutError, URLError) as exc:
170
+ return False, str(exc)
171
+ suffix = ".bat" if url.endswith(".bat") else ".sh"
172
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as handle:
173
+ handle.write(payload)
174
+ return True, handle.name
175
+
176
+
177
+ def install_tinytex(home: Path | None = None) -> dict[str, Any]:
178
+ current = inspect_latex_runtime(home)
179
+ if current.get("tinytex", {}).get("installed"):
180
+ return {
181
+ "ok": True,
182
+ "changed": False,
183
+ "summary": "TinyTeX-managed pdflatex is already installed.",
184
+ "runtime": current,
185
+ }
186
+
187
+ if sys.platform.startswith("win"):
188
+ ok, installer_path_or_error = _download_installer(_TINYTEX_WINDOWS_INSTALLER_URL)
189
+ if not ok:
190
+ return {
191
+ "ok": False,
192
+ "changed": False,
193
+ "summary": "Failed to download the TinyTeX Windows installer.",
194
+ "errors": [installer_path_or_error],
195
+ "guidance": [f"Open {_TINYTEX_DOC_URL} for the official manual install instructions."],
196
+ }
197
+ command = ["cmd", "/c", installer_path_or_error]
198
+ else:
199
+ if not which("sh"):
200
+ return {
201
+ "ok": False,
202
+ "changed": False,
203
+ "summary": "TinyTeX installation requires `/bin/sh` or a compatible shell.",
204
+ "errors": ["`sh` is not available on PATH."],
205
+ "guidance": [f"Install TinyTeX manually from {_TINYTEX_DOC_URL}."],
206
+ }
207
+ if not which("perl"):
208
+ return {
209
+ "ok": False,
210
+ "changed": False,
211
+ "summary": "TinyTeX installation requires Perl on Linux and macOS.",
212
+ "errors": ["`perl` is not available on PATH."],
213
+ "guidance": [
214
+ "Install Perl first, then rerun `ds latex install-runtime`.",
215
+ f"Official docs: {_TINYTEX_DOC_URL}",
216
+ ],
217
+ }
218
+ ok, installer_path_or_error = _download_installer(_TINYTEX_UNIX_INSTALLER_URL)
219
+ if not ok:
220
+ return {
221
+ "ok": False,
222
+ "changed": False,
223
+ "summary": "Failed to download the TinyTeX installer.",
224
+ "errors": [installer_path_or_error],
225
+ "guidance": [f"Open {_TINYTEX_DOC_URL} for the official manual install instructions."],
226
+ }
227
+ command = ["sh", installer_path_or_error]
228
+
229
+ installer_path = Path(installer_path_or_error)
230
+ try:
231
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
232
+ finally:
233
+ installer_path.unlink(missing_ok=True)
234
+ refreshed = inspect_latex_runtime(home)
235
+
236
+ stdout_tail = "\n".join(str(result.stdout or "").splitlines()[-40:])
237
+ stderr_tail = "\n".join(str(result.stderr or "").splitlines()[-40:])
238
+ if result.returncode != 0:
239
+ return {
240
+ "ok": False,
241
+ "changed": False,
242
+ "summary": "TinyTeX installer exited with a non-zero status.",
243
+ "errors": [stderr_tail or stdout_tail or f"Installer exited with status {result.returncode}."],
244
+ "guidance": [
245
+ "Retry `ds latex install-runtime` after checking network connectivity.",
246
+ f"Official docs: {_TINYTEX_DOC_URL}",
247
+ ],
248
+ "exit_code": result.returncode,
249
+ "stdout_tail": stdout_tail,
250
+ "stderr_tail": stderr_tail,
251
+ "runtime": refreshed,
252
+ }
253
+
254
+ if not refreshed.get("tinytex", {}).get("installed"):
255
+ return {
256
+ "ok": False,
257
+ "changed": False,
258
+ "summary": "TinyTeX installer finished, but DeepScientist could not find the managed pdflatex runtime afterward.",
259
+ "errors": ["Installation did not expose a discoverable TinyTeX `pdflatex` binary."],
260
+ "guidance": [
261
+ "Open the TinyTeX documentation and verify the install location.",
262
+ f"Official docs: {_TINYTEX_DOC_URL}",
263
+ ],
264
+ "stdout_tail": stdout_tail,
265
+ "stderr_tail": stderr_tail,
266
+ "runtime": refreshed,
267
+ }
268
+
269
+ return {
270
+ "ok": True,
271
+ "changed": True,
272
+ "summary": "TinyTeX-managed pdflatex is ready.",
273
+ "stdout_tail": stdout_tail,
274
+ "stderr_tail": stderr_tail,
275
+ "runtime": refreshed,
276
+ }
@@ -0,0 +1,12 @@
1
+ # Lingzhu Connector Contract
2
+
3
+ - connector_contract_id: lingzhu
4
+ - connector_contract_scope: loaded only when Lingzhu is the active or bound external connector for this quest
5
+ - connector_contract_goal: keep `artifact.interact(...)` as the main durable conversation spine while optionally requesting device-side actions through `surface_actions`
6
+ - lingzhu_runtime_ack_rule: the Lingzhu bridge itself emits the immediate transport-level receipt acknowledgement before the model turn starts
7
+ - lingzhu_no_duplicate_ack_rule: do not waste your first model response or first `artifact.interact(...)` call on a redundant receipt-only acknowledgement such as "received" or "I am processing" when the bridge already sent that
8
+ - lingzhu_surface_actions_rule: when a device-side step materially helps the current task, request it through `artifact.interact(surface_actions=[...])` rather than inventing ad hoc tool syntax
9
+ - lingzhu_surface_actions_supported: `take_photo`, `send_notification`, `send_toast`, `speak_tts`, `open_custom_view`
10
+ - lingzhu_progress_rule: for long-running work, your first substantive reply should contain either the direct answer or the first concrete checkpoint, not a duplicate transport acknowledgement
11
+ - lingzhu_safety_rule: request only actions that are clearly justified by the current quest and understandable to the human user
12
+ - lingzhu_text_rule: even when requesting `surface_actions`, always include a clear text explanation of what is happening and why