@researai/deepscientist 1.5.12 → 1.5.14
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/bin/ds.js +20 -3
- package/docs/en/00_QUICK_START.md +24 -5
- package/docs/en/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/09_DOCTOR.md +24 -5
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/en/README.md +2 -0
- package/docs/zh/00_QUICK_START.md +24 -5
- package/docs/zh/01_SETTINGS_REFERENCE.md +4 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/09_DOCTOR.md +24 -5
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +113 -15
- package/docs/zh/README.md +2 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +125 -2
- package/src/deepscientist/cli.py +3 -0
- package/src/deepscientist/codex_cli_compat.py +117 -0
- package/src/deepscientist/config/service.py +53 -6
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/daemon/app.py +111 -30
- package/src/deepscientist/mcp/server.py +161 -19
- package/src/deepscientist/prompts/builder.py +13 -54
- package/src/deepscientist/quest/service.py +99 -0
- package/src/deepscientist/quest/stage_views.py +134 -29
- package/src/deepscientist/runners/codex.py +11 -2
- package/src/deepscientist/runners/runtime_overrides.py +3 -0
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/system.md +220 -2065
- package/src/skills/baseline/SKILL.md +265 -994
- package/src/skills/baseline/references/artifact-payload-examples.md +39 -0
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/tui/dist/app/AppContainer.js +1442 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectorConfig.js +90 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/lib/qr.js +21 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +2 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DaF9Nge_.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-BSVx6dXE.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-CB1YODQn.js → CliPlugin-C9gzJX41.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-DU9G0Tox.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-DoX_fI9l.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-C4FWIXuU.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-BgfFMgtf.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-tcPkfY_x.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-_dKV60Bf.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-Bje0ayoC.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CVsBzAln.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-xjmrqv_8.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-mMM2A8wP.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-3kVDSOBo.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-SoJ8X-MO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-DElVuHl9.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-Bq88XT4G.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-CsCXMo9S.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-oUPvy19k.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-CRkT9yNy.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-BgbuvWhR.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-v_RASACv.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-5hC9d0VH.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-D1PxfOrp.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DG1oT_Hj.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-BmdFYQlk.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-Dqe2X2tW.js} +1 -1
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-DVsMKK_y.js} +1 -1
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-Duvz8Ip0.js} +12 -12
- package/src/ui/dist/assets/{index-CLQauncb.js → index-Nt9hS4ck.js} +470 -165
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-RDlNXXx1.js} +2 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-DIXge1CP.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BBTTQaO-.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-BWlolyxo.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-BM5PkFH4.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-D4dAtrA8.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-CKbE5jJT.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-CZNGMgiB.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-DaB37xAz.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-C2OmAcWe.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-Dowd1Ij4.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-BGjAhAUq.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-dMZQMXzc.js} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/uv.lock +1 -1
|
@@ -25,12 +25,19 @@ ds
|
|
|
25
25
|
|
|
26
26
|
### 2. 临时使用 provider profile
|
|
27
27
|
|
|
28
|
-
如果你已经有一个可用的 Codex profile,例如 `
|
|
28
|
+
如果你已经有一个可用的 Codex profile,例如 `m27`、`glm`、`ark`、`bailian`,最简单的方式就是直接在启动 `ds` 时透传它。
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
codex --profile
|
|
32
|
-
ds doctor --codex-profile
|
|
33
|
-
ds --codex-profile
|
|
31
|
+
codex --profile m27
|
|
32
|
+
ds doctor --codex-profile m27
|
|
33
|
+
ds --codex-profile m27
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
如果你这一轮要强制指定某一个 Codex 可执行文件,也可以这样:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ds doctor --codex /absolute/path/to/codex --codex-profile m27
|
|
40
|
+
ds --codex /absolute/path/to/codex --codex-profile m27
|
|
34
41
|
```
|
|
35
42
|
|
|
36
43
|
这是最简单的路径。只是临时试用某个 provider 时,不需要先改 `runners.yaml`。
|
|
@@ -62,7 +69,7 @@ codex:
|
|
|
62
69
|
| Provider | 官方文档 | 是否需要 Codex 登录 | DeepScientist 应该怎么用 |
|
|
63
70
|
|---|---|---|---|
|
|
64
71
|
| OpenAI | 正常 Codex 配置即可 | 是 | 不需要 profile,直接 `ds` |
|
|
65
|
-
| MiniMax | [MiniMax Codex CLI](https://platform.minimaxi.com/docs/coding-plan/codex-cli) | 否 | 使用你自己的 Codex profile,例如 `ds --codex-profile
|
|
72
|
+
| MiniMax | [MiniMax Codex CLI](https://platform.minimaxi.com/docs/coding-plan/codex-cli) | 否 | 使用你自己的 Codex profile,例如 `ds --codex-profile m27` |
|
|
66
73
|
| GLM | [GLM Coding Plan:其他工具](https://docs.bigmodel.cn/cn/coding-plan/tool/others) | 否 | 使用一个指向 GLM coding endpoint 的 Codex profile |
|
|
67
74
|
| 火山方舟 | [Ark Coding Plan 总览](https://www.volcengine.com/docs/82379/1925114?lang=zh) | 否 | 使用一个指向 Ark coding endpoint 的 Codex profile |
|
|
68
75
|
| 阿里百炼 | [百炼 Coding Plan:其他工具](https://help.aliyun.com/zh/model-studio/other-tools-coding-plan) | 否 | 使用一个指向 Bailian coding endpoint 的 Codex profile |
|
|
@@ -100,15 +107,65 @@ MiniMax 是最典型的 profile 模式。它的官方 Codex CLI 文档直接给
|
|
|
100
107
|
|
|
101
108
|
- <https://platform.minimaxi.com/docs/coding-plan/codex-cli>
|
|
102
109
|
|
|
110
|
+
### 已验证的兼容性说明
|
|
111
|
+
|
|
112
|
+
按 2026-03-25 对 MiniMax 官方 Codex CLI 页面和本地兼容性测试的核对结果:
|
|
113
|
+
|
|
114
|
+
- MiniMax 官方 Codex CLI 页面当前建议使用 `@openai/codex@0.57.0`
|
|
115
|
+
- MiniMax 当前应使用的 Coding Plan endpoint 是 `https://api.minimaxi.com/v1`
|
|
116
|
+
- MiniMax 官方页面示例 profile 名是 `m21`,但 profile 名本身只是本地别名;本仓库统一用 `m27` 作为示例名
|
|
117
|
+
- MiniMax 官方页面当前给出的 `codex-MiniMax-*` 模型名,在本地使用你提供的 key 实测并不能稳定通过 Codex CLI
|
|
118
|
+
- 本地实测能稳定跑通的组合是 `MiniMax-M2.7` + `m27` + `model: inherit` + Codex CLI `0.57.0`
|
|
119
|
+
- 当前最新版 `@openai/codex` 和 MiniMax 官方文档并不能稳定直接对齐
|
|
120
|
+
|
|
121
|
+
如果你现在要走最稳的 DeepScientist + MiniMax 路径,建议直接使用 Codex CLI `0.57.0`。
|
|
122
|
+
|
|
103
123
|
### 需要准备什么
|
|
104
124
|
|
|
105
|
-
- 已安装 Codex CLI
|
|
125
|
+
- 已安装 Codex CLI `0.57.0`
|
|
126
|
+
- 已创建 MiniMax `Coding Plan Key`
|
|
106
127
|
- 在启动 Codex 和 DeepScientist 的 shell 中可见的 `MINIMAX_API_KEY`
|
|
128
|
+
- 当前 shell 已清理 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL`
|
|
107
129
|
- `~/.codex/config.toml` 中已经配置好的 Codex profile
|
|
108
130
|
|
|
131
|
+
### 安装 Codex CLI `0.57.0`
|
|
132
|
+
|
|
133
|
+
最直接的方式是把全局 Codex 安装固定到 `0.57.0`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install -g @openai/codex@0.57.0
|
|
137
|
+
codex --version
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
预期输出:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
codex-cli 0.57.0
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
如果你还想保留另一个 Codex 版本,也可以单独写一个 wrapper 脚本,再把 `runners.codex.binary` 指向那个绝对路径。
|
|
147
|
+
|
|
109
148
|
### Codex 侧配置
|
|
110
149
|
|
|
111
|
-
|
|
150
|
+
请使用 `https://api.minimaxi.com/v1`,不要用 `https://api.minimax.io/v1`。
|
|
151
|
+
|
|
152
|
+
MiniMax 官方文档要求在配置前先清理 OpenAI 环境变量:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
unset OPENAI_API_KEY
|
|
156
|
+
unset OPENAI_BASE_URL
|
|
157
|
+
export MINIMAX_API_KEY="..."
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
MiniMax 官方页面示例 profile 名是 `m21`。由于 profile 名只是本地别名,本仓库统一改写成 `m27`。
|
|
161
|
+
|
|
162
|
+
先说明差异:
|
|
163
|
+
|
|
164
|
+
- 官方页面当前展示的是 `codex-MiniMax-M2.5`
|
|
165
|
+
- 但本地实测里,直接请求 MiniMax API 能稳定跑通的是 `MiniMax-M2.7`
|
|
166
|
+
- 同一把 key 下,`codex-MiniMax-M2.5` / `codex-MiniMax-M2.7` 通过 Codex CLI 都会失败
|
|
167
|
+
|
|
168
|
+
因此,下面给的是当前 DeepScientist 推荐的可运行配置:
|
|
112
169
|
|
|
113
170
|
```toml
|
|
114
171
|
[model_providers.minimax]
|
|
@@ -121,23 +178,50 @@ request_max_retries = 4
|
|
|
121
178
|
stream_max_retries = 10
|
|
122
179
|
stream_idle_timeout_ms = 300000
|
|
123
180
|
|
|
124
|
-
[profiles.
|
|
125
|
-
model = "
|
|
181
|
+
[profiles.m27]
|
|
182
|
+
model = "MiniMax-M2.7"
|
|
183
|
+
model_provider = "minimax"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
DeepScientist 现在对它的支持方式是:
|
|
187
|
+
|
|
188
|
+
- 如果你使用的是这类 profile-only MiniMax 配置,再配合 Codex CLI `0.57.0`,DeepScientist 会在自己的 probe / 运行时临时 `.codex/config.toml` 里,把所选 profile 的 `model_provider` 和 `model` 自动提升到顶层
|
|
189
|
+
- 这意味着即使终端里原样执行 `codex --profile m27` 还会失败,DeepScientist 也可以先兼容跑起来
|
|
190
|
+
|
|
191
|
+
如果你还希望终端里的 `codex --profile <name>` 也直接可用,请使用显式顶层兼容写法:
|
|
192
|
+
|
|
193
|
+
```toml
|
|
194
|
+
model = "MiniMax-M2.7"
|
|
195
|
+
model_provider = "minimax"
|
|
196
|
+
approval_policy = "never"
|
|
197
|
+
sandbox_mode = "workspace-write"
|
|
198
|
+
|
|
199
|
+
[model_providers.minimax]
|
|
200
|
+
name = "MiniMax Chat Completions API"
|
|
201
|
+
base_url = "https://api.minimaxi.com/v1"
|
|
202
|
+
env_key = "MINIMAX_API_KEY"
|
|
203
|
+
wire_api = "chat"
|
|
204
|
+
requires_openai_auth = false
|
|
205
|
+
request_max_retries = 4
|
|
206
|
+
stream_max_retries = 10
|
|
207
|
+
stream_idle_timeout_ms = 300000
|
|
208
|
+
|
|
209
|
+
[profiles.m27]
|
|
210
|
+
model = "MiniMax-M2.7"
|
|
126
211
|
model_provider = "minimax"
|
|
127
212
|
```
|
|
128
213
|
|
|
129
214
|
然后执行:
|
|
130
215
|
|
|
131
216
|
```bash
|
|
132
|
-
|
|
133
|
-
codex --profile minimax
|
|
217
|
+
codex --profile m27
|
|
134
218
|
```
|
|
135
219
|
|
|
136
220
|
### DeepScientist 命令
|
|
137
221
|
|
|
138
222
|
```bash
|
|
139
|
-
ds doctor --codex-profile
|
|
140
|
-
ds --codex-profile
|
|
223
|
+
ds doctor --codex-profile m27
|
|
224
|
+
ds --codex-profile m27
|
|
141
225
|
```
|
|
142
226
|
|
|
143
227
|
### 持久化 runner 配置
|
|
@@ -145,12 +229,26 @@ ds --codex-profile minimax
|
|
|
145
229
|
```yaml
|
|
146
230
|
codex:
|
|
147
231
|
enabled: true
|
|
148
|
-
binary:
|
|
232
|
+
binary: /tmp/codex057-wrapper
|
|
149
233
|
config_dir: ~/.codex
|
|
150
|
-
profile:
|
|
234
|
+
profile: m27
|
|
151
235
|
model: inherit
|
|
236
|
+
model_reasoning_effort: high
|
|
152
237
|
```
|
|
153
238
|
|
|
239
|
+
如果你已经把全局 `codex` 固定到 `0.57.0`,也可以把 `binary` 写回 `codex`。这里写绝对路径只是为了明确避免误用系统里其他版本的 Codex。
|
|
240
|
+
|
|
241
|
+
如果你不想把这个路径持久化写进 `runners.yaml`,也可以保留 `binary: codex`,然后在启动时临时加:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
ds --codex /absolute/path/to/codex --codex-profile m27
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
DeepScientist 现在会为 MiniMax 的 `0.57.0` 路径额外做两层兼容:
|
|
248
|
+
|
|
249
|
+
- 当检测到旧版 Codex CLI 不支持 `xhigh` 时,自动把 `xhigh` 降级成 `high`
|
|
250
|
+
- 当检测到 MiniMax 使用 profile-only 的 `model_provider` / `model` 配置形态时,在临时 DeepScientist Codex home 里自动补齐顶层字段
|
|
251
|
+
|
|
154
252
|
## GLM
|
|
155
253
|
|
|
156
254
|
GLM 的官方文档把 Coding Plan 描述成 OpenAI-compatible 的 coding endpoint,而不是单独的 Codex 登录流程。
|
package/docs/zh/README.md
CHANGED
|
@@ -76,6 +76,8 @@ DeepScientist 灵活且易于使用,支持:
|
|
|
76
76
|
|
|
77
77
|
- [00 快速开始](./00_QUICK_START.md)
|
|
78
78
|
从安装、启动,到创建第一个项目,先看这一篇。
|
|
79
|
+
- [05 TUI 端到端指南](./05_TUI_GUIDE.md)
|
|
80
|
+
如果你主要在服务器或终端里工作,这篇会带你从 `ds --tui` 一路走到 quest、connector 和跨端协作跑通。
|
|
79
81
|
- [15 Codex Provider 配置](./15_CODEX_PROVIDER_SETUP.md)
|
|
80
82
|
如果你准备通过 MiniMax、GLM、火山方舟、阿里百炼或其他 Codex profile 来运行 DeepScientist,先看这一篇。
|
|
81
83
|
- [12 引导式工作流教程](./12_GUIDED_WORKFLOW_TOUR.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@researai/deepscientist",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.14",
|
|
4
4
|
"description": "DeepScientist is not just a fully open-source autonomous scientific discovery system. It is also a research map that keeps growing from every round.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@openai/codex": "^0.114.0",
|
|
39
39
|
"ink": "npm:@jrichman/ink@6.4.6",
|
|
40
40
|
"ink-gradient": "^3.0.0",
|
|
41
|
+
"qrcode": "^1.5.4",
|
|
41
42
|
"react": "^19.2.0",
|
|
42
43
|
"react-dom": "^19.2.0",
|
|
43
44
|
"string-width": "^8.1.0"
|
package/pyproject.toml
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import shutil
|
|
5
|
-
from pathlib import Path
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from ..arxiv_library import ArxivLibraryService
|
|
@@ -693,6 +693,29 @@ class ArtifactService:
|
|
|
693
693
|
except ValueError:
|
|
694
694
|
return str(path)
|
|
695
695
|
|
|
696
|
+
def _paper_bundle_relative_path(
|
|
697
|
+
self,
|
|
698
|
+
quest_root: Path,
|
|
699
|
+
path: Path | None,
|
|
700
|
+
*,
|
|
701
|
+
workspace_root: Path | None = None,
|
|
702
|
+
) -> str | None:
|
|
703
|
+
if path is None:
|
|
704
|
+
return None
|
|
705
|
+
resolved = path.resolve()
|
|
706
|
+
roots = [self._workspace_root_for(quest_root, workspace_root), quest_root]
|
|
707
|
+
seen: set[str] = set()
|
|
708
|
+
for root in roots:
|
|
709
|
+
key = str(root.resolve())
|
|
710
|
+
if key in seen:
|
|
711
|
+
continue
|
|
712
|
+
seen.add(key)
|
|
713
|
+
try:
|
|
714
|
+
return resolved.relative_to(root.resolve()).as_posix()
|
|
715
|
+
except ValueError:
|
|
716
|
+
continue
|
|
717
|
+
return str(path)
|
|
718
|
+
|
|
696
719
|
@staticmethod
|
|
697
720
|
def _branch_kind_from_name(branch_name: str | None) -> str:
|
|
698
721
|
normalized = str(branch_name or "").strip()
|
|
@@ -1289,6 +1312,100 @@ class ArtifactService:
|
|
|
1289
1312
|
def _paper_baseline_inventory_path(self, quest_root: Path, *, workspace_root: Path | None = None) -> Path:
|
|
1290
1313
|
return self._paper_root(quest_root, workspace_root=workspace_root, create=True) / "baseline_inventory.json"
|
|
1291
1314
|
|
|
1315
|
+
def _paper_bundle_path_candidates(
|
|
1316
|
+
self,
|
|
1317
|
+
quest_root: Path,
|
|
1318
|
+
raw_path: object,
|
|
1319
|
+
*,
|
|
1320
|
+
workspace_root: Path | None = None,
|
|
1321
|
+
) -> list[Path]:
|
|
1322
|
+
text = str(raw_path or "").strip()
|
|
1323
|
+
if not text:
|
|
1324
|
+
return []
|
|
1325
|
+
candidate = Path(text).expanduser()
|
|
1326
|
+
roots = [self._workspace_root_for(quest_root, workspace_root), quest_root]
|
|
1327
|
+
resolved: list[Path] = []
|
|
1328
|
+
if candidate.is_absolute():
|
|
1329
|
+
try:
|
|
1330
|
+
resolved.append(candidate.resolve())
|
|
1331
|
+
except OSError:
|
|
1332
|
+
return []
|
|
1333
|
+
else:
|
|
1334
|
+
for root in roots:
|
|
1335
|
+
try:
|
|
1336
|
+
resolved.append((root / candidate).resolve())
|
|
1337
|
+
except OSError:
|
|
1338
|
+
continue
|
|
1339
|
+
deduped: list[Path] = []
|
|
1340
|
+
seen: set[str] = set()
|
|
1341
|
+
for item in resolved:
|
|
1342
|
+
key = str(item)
|
|
1343
|
+
if key in seen:
|
|
1344
|
+
continue
|
|
1345
|
+
seen.add(key)
|
|
1346
|
+
deduped.append(item)
|
|
1347
|
+
return deduped
|
|
1348
|
+
|
|
1349
|
+
def _paper_bundle_compile_report(
|
|
1350
|
+
self,
|
|
1351
|
+
quest_root: Path,
|
|
1352
|
+
*,
|
|
1353
|
+
workspace_root: Path | None = None,
|
|
1354
|
+
compile_report_path: object = None,
|
|
1355
|
+
) -> dict[str, Any]:
|
|
1356
|
+
for candidate in self._paper_bundle_path_candidates(
|
|
1357
|
+
quest_root,
|
|
1358
|
+
compile_report_path,
|
|
1359
|
+
workspace_root=workspace_root,
|
|
1360
|
+
):
|
|
1361
|
+
if not candidate.exists() or not candidate.is_file():
|
|
1362
|
+
continue
|
|
1363
|
+
payload = read_json(candidate, {})
|
|
1364
|
+
if isinstance(payload, dict):
|
|
1365
|
+
return payload
|
|
1366
|
+
return {}
|
|
1367
|
+
|
|
1368
|
+
def _normalize_paper_bundle_latex_root_path(
|
|
1369
|
+
self,
|
|
1370
|
+
quest_root: Path,
|
|
1371
|
+
*,
|
|
1372
|
+
workspace_root: Path | None = None,
|
|
1373
|
+
latex_root_path: object = None,
|
|
1374
|
+
compile_report_path: object = None,
|
|
1375
|
+
) -> str | None:
|
|
1376
|
+
compile_report = self._paper_bundle_compile_report(
|
|
1377
|
+
quest_root,
|
|
1378
|
+
workspace_root=workspace_root,
|
|
1379
|
+
compile_report_path=compile_report_path,
|
|
1380
|
+
)
|
|
1381
|
+
for raw in (
|
|
1382
|
+
latex_root_path,
|
|
1383
|
+
compile_report.get("latex_root_path"),
|
|
1384
|
+
compile_report.get("main_file_path"),
|
|
1385
|
+
):
|
|
1386
|
+
text = str(raw or "").strip()
|
|
1387
|
+
if not text:
|
|
1388
|
+
continue
|
|
1389
|
+
for candidate in self._paper_bundle_path_candidates(
|
|
1390
|
+
quest_root,
|
|
1391
|
+
text,
|
|
1392
|
+
workspace_root=workspace_root,
|
|
1393
|
+
):
|
|
1394
|
+
if candidate.exists() and candidate.is_dir():
|
|
1395
|
+
return self._paper_bundle_relative_path(quest_root, candidate, workspace_root=workspace_root) or text
|
|
1396
|
+
if candidate.suffix.lower() == ".tex":
|
|
1397
|
+
return self._paper_bundle_relative_path(
|
|
1398
|
+
quest_root,
|
|
1399
|
+
candidate.parent,
|
|
1400
|
+
workspace_root=workspace_root,
|
|
1401
|
+
) or PurePosixPath(text).parent.as_posix()
|
|
1402
|
+
if Path(text).suffix.lower() == ".tex":
|
|
1403
|
+
parent = PurePosixPath(text).parent.as_posix()
|
|
1404
|
+
if parent not in {"", "."}:
|
|
1405
|
+
return parent
|
|
1406
|
+
return text
|
|
1407
|
+
return None
|
|
1408
|
+
|
|
1292
1409
|
def _open_source_root(
|
|
1293
1410
|
self,
|
|
1294
1411
|
quest_root: Path,
|
|
@@ -5696,6 +5813,12 @@ class ArtifactService:
|
|
|
5696
5813
|
default_compile_report_path = (
|
|
5697
5814
|
self._workspace_relative(quest_root, paper_root / "build" / "compile_report.json") or "paper/build/compile_report.json"
|
|
5698
5815
|
)
|
|
5816
|
+
normalized_latex_root_path = self._normalize_paper_bundle_latex_root_path(
|
|
5817
|
+
quest_root,
|
|
5818
|
+
workspace_root=workspace_root,
|
|
5819
|
+
latex_root_path=latex_root_path,
|
|
5820
|
+
compile_report_path=compile_report_path or default_compile_report_path,
|
|
5821
|
+
)
|
|
5699
5822
|
manifest = {
|
|
5700
5823
|
"schema_version": 1,
|
|
5701
5824
|
"title": str(
|
|
@@ -5717,7 +5840,7 @@ class ArtifactService:
|
|
|
5717
5840
|
"claim_evidence_map_path": str(claim_evidence_map_path or default_claim_map_path).strip() or None,
|
|
5718
5841
|
"compile_report_path": str(compile_report_path or default_compile_report_path).strip() or None,
|
|
5719
5842
|
"pdf_path": str(pdf_path or "").strip() or None,
|
|
5720
|
-
"latex_root_path":
|
|
5843
|
+
"latex_root_path": normalized_latex_root_path,
|
|
5721
5844
|
"baseline_inventory_path": paper_inventory_rel,
|
|
5722
5845
|
"open_source_manifest_path": self._workspace_relative(
|
|
5723
5846
|
quest_root,
|
package/src/deepscientist/cli.py
CHANGED
|
@@ -39,6 +39,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
39
39
|
parser = argparse.ArgumentParser(prog="ds", description="DeepScientist Core skeleton")
|
|
40
40
|
parser.add_argument("--home", default=None, help="Override DeepScientist home")
|
|
41
41
|
parser.add_argument("--proxy", default=None, help="Explicit outbound HTTP/WS proxy, for example `http://127.0.0.1:7890`.")
|
|
42
|
+
parser.add_argument("--codex", default=None, help="Override the Codex executable path for this invocation.")
|
|
42
43
|
|
|
43
44
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
44
45
|
|
|
@@ -475,6 +476,8 @@ def migrate_command(home: Path, target: str) -> int:
|
|
|
475
476
|
def main(argv: list[str] | None = None) -> int:
|
|
476
477
|
parser = build_parser()
|
|
477
478
|
args = parser.parse_args(argv)
|
|
479
|
+
if args.codex:
|
|
480
|
+
os.environ["DEEPSCIENTIST_CODEX_BINARY"] = str(args.codex)
|
|
478
481
|
configure_runtime_proxy(args.proxy)
|
|
479
482
|
home = resolve_home(args)
|
|
480
483
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import tomllib
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
|
|
9
|
+
_MIN_XHIGH_SUPPORTED_VERSION = (0, 63, 0)
|
|
10
|
+
_CODEX_VERSION_PATTERN = re.compile(r"codex-cli\s+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_codex_cli_version(text: str) -> tuple[int, int, int] | None:
|
|
14
|
+
match = _CODEX_VERSION_PATTERN.search(str(text or ""))
|
|
15
|
+
if not match:
|
|
16
|
+
return None
|
|
17
|
+
return tuple(int(part) for part in match.groups())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@lru_cache(maxsize=32)
|
|
21
|
+
def codex_cli_version(binary: str) -> tuple[int, int, int] | None:
|
|
22
|
+
normalized = str(binary or "").strip()
|
|
23
|
+
if not normalized:
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
[normalized, "--version"],
|
|
28
|
+
check=False,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=10,
|
|
32
|
+
)
|
|
33
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
34
|
+
return None
|
|
35
|
+
return parse_codex_cli_version(f"{result.stdout}\n{result.stderr}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_codex_cli_version(version: tuple[int, int, int] | None) -> str:
|
|
39
|
+
if version is None:
|
|
40
|
+
return ""
|
|
41
|
+
return ".".join(str(part) for part in version)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_codex_reasoning_effort(
|
|
45
|
+
reasoning_effort: str | None,
|
|
46
|
+
*,
|
|
47
|
+
resolved_binary: str | None,
|
|
48
|
+
) -> tuple[str | None, str | None]:
|
|
49
|
+
normalized = str(reasoning_effort or "").strip()
|
|
50
|
+
if not normalized:
|
|
51
|
+
return None, None
|
|
52
|
+
if normalized.lower() != "xhigh":
|
|
53
|
+
return normalized, None
|
|
54
|
+
|
|
55
|
+
version = codex_cli_version(str(resolved_binary or ""))
|
|
56
|
+
if version is None or version >= _MIN_XHIGH_SUPPORTED_VERSION:
|
|
57
|
+
return normalized, None
|
|
58
|
+
|
|
59
|
+
version_text = format_codex_cli_version(version)
|
|
60
|
+
return (
|
|
61
|
+
"high",
|
|
62
|
+
(
|
|
63
|
+
f"Codex CLI {version_text} does not support `xhigh`; "
|
|
64
|
+
"DeepScientist downgraded reasoning effort to `high` automatically."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def adapt_profile_only_provider_config(
|
|
70
|
+
config_text: str,
|
|
71
|
+
*,
|
|
72
|
+
profile: str,
|
|
73
|
+
) -> tuple[str, str | None]:
|
|
74
|
+
normalized_profile = str(profile or "").strip()
|
|
75
|
+
if not normalized_profile or not str(config_text or "").strip():
|
|
76
|
+
return config_text, None
|
|
77
|
+
try:
|
|
78
|
+
parsed = tomllib.loads(config_text)
|
|
79
|
+
except tomllib.TOMLDecodeError:
|
|
80
|
+
return config_text, None
|
|
81
|
+
|
|
82
|
+
profiles = parsed.get("profiles")
|
|
83
|
+
if not isinstance(profiles, dict):
|
|
84
|
+
return config_text, None
|
|
85
|
+
profile_payload = profiles.get(normalized_profile)
|
|
86
|
+
if not isinstance(profile_payload, dict):
|
|
87
|
+
return config_text, None
|
|
88
|
+
|
|
89
|
+
prefix_lines: list[str] = []
|
|
90
|
+
injected_fields: list[str] = []
|
|
91
|
+
if "model_provider" not in parsed:
|
|
92
|
+
model_provider = str(profile_payload.get("model_provider") or "").strip()
|
|
93
|
+
if model_provider:
|
|
94
|
+
prefix_lines.append(f"model_provider = {json.dumps(model_provider, ensure_ascii=False)}")
|
|
95
|
+
injected_fields.append("model_provider")
|
|
96
|
+
if "model" not in parsed:
|
|
97
|
+
model = str(profile_payload.get("model") or "").strip()
|
|
98
|
+
if model:
|
|
99
|
+
prefix_lines.append(f"model = {json.dumps(model, ensure_ascii=False)}")
|
|
100
|
+
injected_fields.append("model")
|
|
101
|
+
|
|
102
|
+
if not prefix_lines:
|
|
103
|
+
return config_text, None
|
|
104
|
+
|
|
105
|
+
adapted = (
|
|
106
|
+
"# BEGIN DEEPSCIENTIST PROFILE COMPAT\n"
|
|
107
|
+
+ "\n".join(prefix_lines)
|
|
108
|
+
+ "\n# END DEEPSCIENTIST PROFILE COMPAT\n\n"
|
|
109
|
+
+ config_text.lstrip()
|
|
110
|
+
)
|
|
111
|
+
return (
|
|
112
|
+
adapted,
|
|
113
|
+
(
|
|
114
|
+
f"DeepScientist promoted `{normalized_profile}` profile "
|
|
115
|
+
f"{', '.join(injected_fields)} to the top level for Codex compatibility."
|
|
116
|
+
),
|
|
117
|
+
)
|
|
@@ -3,11 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from shutil import copy2
|
|
6
8
|
from copy import deepcopy
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from urllib.error import URLError
|
|
9
11
|
from urllib.request import Request
|
|
10
12
|
|
|
13
|
+
from ..codex_cli_compat import adapt_profile_only_provider_config, normalize_codex_reasoning_effort
|
|
11
14
|
from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
|
|
12
15
|
from ..connector_runtime import build_discovered_target, infer_connector_transport
|
|
13
16
|
from ..home import repo_root
|
|
@@ -486,6 +489,7 @@ This page edits `{home_text}/config/runners.yaml`.
|
|
|
486
489
|
- `claude` remains TODO / reserved in the current open-source release and is not runnable yet
|
|
487
490
|
- set `codex.profile` only when your Codex CLI uses a named provider profile such as `m27`
|
|
488
491
|
- when you launch DeepScientist ad hoc with a provider profile, you can also use `ds --codex-profile <name>`
|
|
492
|
+
- when you want a one-off Codex binary override, you can also use `ds --codex /absolute/path/to/codex`
|
|
489
493
|
- keep `codex.model_reasoning_effort: xhigh` unless you explicitly want a lighter default
|
|
490
494
|
- keep `codex.retry_on_failure: true` so transient Codex failures can resume automatically
|
|
491
495
|
- keep retry timing near `10s / 6x / 1800s max` so Codex backs off exponentially and the last retry waits about 30 minutes
|
|
@@ -1206,6 +1210,31 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1206
1210
|
resolved[env_key] = str(value)
|
|
1207
1211
|
return resolved
|
|
1208
1212
|
|
|
1213
|
+
def _prepare_codex_probe_home(
|
|
1214
|
+
self,
|
|
1215
|
+
*,
|
|
1216
|
+
config_dir: str,
|
|
1217
|
+
profile: str,
|
|
1218
|
+
) -> tuple[str, str | None, tempfile.TemporaryDirectory[str] | None]:
|
|
1219
|
+
expanded = Path(config_dir).expanduser()
|
|
1220
|
+
config_path = expanded / "config.toml"
|
|
1221
|
+
if not config_path.exists():
|
|
1222
|
+
return str(expanded), None, None
|
|
1223
|
+
|
|
1224
|
+
original_text = read_text(config_path)
|
|
1225
|
+
adapted_text, warning = adapt_profile_only_provider_config(original_text, profile=profile)
|
|
1226
|
+
if warning is None:
|
|
1227
|
+
return str(expanded), None, None
|
|
1228
|
+
|
|
1229
|
+
temp_home = tempfile.TemporaryDirectory(prefix="ds-codex-probe-")
|
|
1230
|
+
temp_root = Path(temp_home.name)
|
|
1231
|
+
for filename in ("auth.json",):
|
|
1232
|
+
source_path = expanded / filename
|
|
1233
|
+
if source_path.exists():
|
|
1234
|
+
copy2(source_path, temp_root / filename)
|
|
1235
|
+
write_text(temp_root / "config.toml", adapted_text)
|
|
1236
|
+
return str(temp_root), warning, temp_home
|
|
1237
|
+
|
|
1209
1238
|
def _codex_missing_binary_guidance(self, config: dict) -> list[str]:
|
|
1210
1239
|
profile = self._codex_profile_name(config)
|
|
1211
1240
|
guidance = [
|
|
@@ -1221,7 +1250,9 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1221
1250
|
)
|
|
1222
1251
|
else:
|
|
1223
1252
|
guidance.append("Run `codex --login` (or `codex`) once and finish authentication before starting DeepScientist.")
|
|
1224
|
-
guidance.append(
|
|
1253
|
+
guidance.append(
|
|
1254
|
+
"If you use a custom Codex path, either set `runners.codex.binary` or launch with `ds --codex /absolute/path/to/codex`."
|
|
1255
|
+
)
|
|
1225
1256
|
return guidance
|
|
1226
1257
|
|
|
1227
1258
|
def _codex_probe_failure_guidance(self, config: dict) -> tuple[list[str], list[str]]:
|
|
@@ -1326,11 +1357,15 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1326
1357
|
profile = self._codex_profile_name(config)
|
|
1327
1358
|
requested_model = self._codex_requested_model(config)
|
|
1328
1359
|
raw_reasoning_effort = config.get("model_reasoning_effort")
|
|
1329
|
-
|
|
1360
|
+
requested_reasoning_effort = (
|
|
1330
1361
|
str(raw_reasoning_effort).strip()
|
|
1331
1362
|
if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
|
|
1332
1363
|
else ("xhigh" if raw_reasoning_effort is None else None)
|
|
1333
1364
|
)
|
|
1365
|
+
reasoning_effort, reasoning_effort_warning = normalize_codex_reasoning_effort(
|
|
1366
|
+
requested_reasoning_effort,
|
|
1367
|
+
resolved_binary=resolved_binary,
|
|
1368
|
+
)
|
|
1334
1369
|
details: dict[str, object] = {
|
|
1335
1370
|
"binary": binary,
|
|
1336
1371
|
"resolved_binary": resolved_binary,
|
|
@@ -1342,6 +1377,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1342
1377
|
"approval_policy": str(config.get("approval_policy") or "on-request"),
|
|
1343
1378
|
"sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
|
|
1344
1379
|
"reasoning_effort": reasoning_effort,
|
|
1380
|
+
"requested_reasoning_effort": requested_reasoning_effort,
|
|
1345
1381
|
"model_fallback_attempted": False,
|
|
1346
1382
|
"model_fallback_used": False,
|
|
1347
1383
|
"checked_at": checked_at,
|
|
@@ -1365,9 +1401,20 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1365
1401
|
env = os.environ.copy()
|
|
1366
1402
|
env.update(self._codex_runner_env(config))
|
|
1367
1403
|
config_dir = str(config.get("config_dir") or "~/.codex").strip()
|
|
1404
|
+
probe_home_handle: tempfile.TemporaryDirectory[str] | None = None
|
|
1405
|
+
compatibility_warnings: list[str] = []
|
|
1368
1406
|
if config_dir:
|
|
1369
|
-
|
|
1407
|
+
prepared_home, profile_config_warning, probe_home_handle = self._prepare_codex_probe_home(
|
|
1408
|
+
config_dir=config_dir,
|
|
1409
|
+
profile=profile,
|
|
1410
|
+
)
|
|
1411
|
+
env["CODEX_HOME"] = prepared_home
|
|
1412
|
+
if profile_config_warning:
|
|
1413
|
+
compatibility_warnings.append(profile_config_warning)
|
|
1370
1414
|
prompt = "Reply with exactly HELLO."
|
|
1415
|
+
if reasoning_effort_warning:
|
|
1416
|
+
compatibility_warnings.append(reasoning_effort_warning)
|
|
1417
|
+
base_warnings: list[str] = list(compatibility_warnings)
|
|
1371
1418
|
|
|
1372
1419
|
def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
|
|
1373
1420
|
command = self._build_codex_probe_command(
|
|
@@ -1406,7 +1453,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1406
1453
|
return {
|
|
1407
1454
|
"ok": False,
|
|
1408
1455
|
"summary": "Codex startup probe timed out.",
|
|
1409
|
-
"warnings":
|
|
1456
|
+
"warnings": base_warnings,
|
|
1410
1457
|
"errors": [
|
|
1411
1458
|
"Codex did not answer the startup hello probe within 90 seconds.",
|
|
1412
1459
|
*self._codex_probe_failure_guidance(config)[0],
|
|
@@ -1463,7 +1510,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1463
1510
|
return {
|
|
1464
1511
|
"ok": True,
|
|
1465
1512
|
"summary": "Codex startup probe completed with Codex default model fallback.",
|
|
1466
|
-
"warnings": [fallback_warning],
|
|
1513
|
+
"warnings": [*base_warnings, fallback_warning],
|
|
1467
1514
|
"errors": [],
|
|
1468
1515
|
"details": details,
|
|
1469
1516
|
"guidance": [
|
|
@@ -1483,7 +1530,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1483
1530
|
"probe_command": command,
|
|
1484
1531
|
}
|
|
1485
1532
|
)
|
|
1486
|
-
warnings: list[str] =
|
|
1533
|
+
warnings: list[str] = list(base_warnings)
|
|
1487
1534
|
errors: list[str] = []
|
|
1488
1535
|
if not ok:
|
|
1489
1536
|
errors.append("Codex did not complete the startup hello probe successfully.")
|