@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.
- package/AGENTS.md +26 -0
- package/README.md +19 -179
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +233 -53
- package/docs/en/00_QUICK_START.md +134 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +108 -0
- package/docs/en/90_ARCHITECTURE.md +245 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +134 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +112 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +32 -8
- package/package.json +4 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +482 -22
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +201 -6
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +50 -5
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +442 -15
- package/src/deepscientist/doctor.py +444 -0
- package/src/deepscientist/home.py +1 -0
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/server.py +49 -2
- package/src/deepscientist/prompts/builder.py +181 -58
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +63 -2
- package/src/deepscientist/quest/stage_views.py +19 -1
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +12 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +177 -33
- package/src/skills/analysis-campaign/SKILL.md +22 -6
- package/src/skills/baseline/SKILL.md +5 -4
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +5 -4
- package/src/skills/finalize/SKILL.md +5 -4
- package/src/skills/idea/SKILL.md +5 -4
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +407 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +293 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +5 -4
- package/src/skills/write/SKILL.md +7 -3
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
- package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|