@researai/deepscientist 1.5.9 → 1.5.12

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 (165) hide show
  1. package/README.md +112 -99
  2. package/assets/branding/connector-qq.png +0 -0
  3. package/assets/branding/connector-rokid.png +0 -0
  4. package/assets/branding/connector-weixin.png +0 -0
  5. package/assets/branding/projects.png +0 -0
  6. package/bin/ds.js +519 -63
  7. package/docs/assets/branding/projects.png +0 -0
  8. package/docs/en/00_QUICK_START.md +338 -68
  9. package/docs/en/01_SETTINGS_REFERENCE.md +14 -0
  10. package/docs/en/02_START_RESEARCH_GUIDE.md +180 -4
  11. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  12. package/docs/en/09_DOCTOR.md +66 -5
  13. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  14. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  15. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +446 -0
  16. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  17. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  18. package/docs/en/15_CODEX_PROVIDER_SETUP.md +284 -0
  19. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  20. package/docs/en/README.md +83 -0
  21. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  23. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  24. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  25. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  26. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  27. package/docs/zh/00_QUICK_START.md +345 -72
  28. package/docs/zh/01_SETTINGS_REFERENCE.md +14 -0
  29. package/docs/zh/02_START_RESEARCH_GUIDE.md +181 -3
  30. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  31. package/docs/zh/09_DOCTOR.md +68 -5
  32. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  33. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  34. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +442 -0
  35. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  36. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  37. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +285 -0
  38. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  39. package/docs/zh/README.md +129 -0
  40. package/install.sh +0 -34
  41. package/package.json +2 -2
  42. package/pyproject.toml +1 -1
  43. package/src/deepscientist/__init__.py +1 -1
  44. package/src/deepscientist/annotations.py +343 -0
  45. package/src/deepscientist/artifact/arxiv.py +484 -37
  46. package/src/deepscientist/artifact/service.py +574 -108
  47. package/src/deepscientist/arxiv_library.py +275 -0
  48. package/src/deepscientist/bash_exec/monitor.py +7 -5
  49. package/src/deepscientist/bash_exec/service.py +93 -21
  50. package/src/deepscientist/bridges/builtins.py +2 -0
  51. package/src/deepscientist/bridges/connectors.py +447 -0
  52. package/src/deepscientist/channels/__init__.py +2 -0
  53. package/src/deepscientist/channels/builtins.py +3 -1
  54. package/src/deepscientist/channels/local.py +3 -3
  55. package/src/deepscientist/channels/qq.py +8 -8
  56. package/src/deepscientist/channels/qq_gateway.py +1 -1
  57. package/src/deepscientist/channels/relay.py +14 -8
  58. package/src/deepscientist/channels/weixin.py +59 -0
  59. package/src/deepscientist/channels/weixin_ilink.py +388 -0
  60. package/src/deepscientist/config/models.py +23 -2
  61. package/src/deepscientist/config/service.py +539 -67
  62. package/src/deepscientist/connector/__init__.py +4 -0
  63. package/src/deepscientist/connector/connector_profiles.py +481 -0
  64. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  65. package/src/deepscientist/connector/qq_profiles.py +206 -0
  66. package/src/deepscientist/connector/weixin_support.py +663 -0
  67. package/src/deepscientist/connector_profiles.py +1 -374
  68. package/src/deepscientist/connector_runtime.py +2 -0
  69. package/src/deepscientist/daemon/api/handlers.py +165 -5
  70. package/src/deepscientist/daemon/api/router.py +13 -1
  71. package/src/deepscientist/daemon/app.py +1444 -67
  72. package/src/deepscientist/doctor.py +4 -5
  73. package/src/deepscientist/gitops/diff.py +120 -29
  74. package/src/deepscientist/lingzhu_support.py +1 -182
  75. package/src/deepscientist/mcp/server.py +135 -7
  76. package/src/deepscientist/prompts/builder.py +128 -11
  77. package/src/deepscientist/qq_profiles.py +1 -196
  78. package/src/deepscientist/quest/node_traces.py +23 -0
  79. package/src/deepscientist/quest/service.py +359 -74
  80. package/src/deepscientist/quest/stage_views.py +71 -5
  81. package/src/deepscientist/runners/codex.py +170 -19
  82. package/src/deepscientist/runners/runtime_overrides.py +6 -0
  83. package/src/deepscientist/shared.py +33 -14
  84. package/src/deepscientist/weixin_support.py +1 -0
  85. package/src/prompts/connectors/lingzhu.md +3 -1
  86. package/src/prompts/connectors/qq.md +2 -1
  87. package/src/prompts/connectors/weixin.md +231 -0
  88. package/src/prompts/contracts/shared_interaction.md +4 -1
  89. package/src/prompts/system.md +61 -9
  90. package/src/skills/analysis-campaign/SKILL.md +46 -6
  91. package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
  92. package/src/skills/baseline/SKILL.md +1 -1
  93. package/src/skills/decision/SKILL.md +1 -1
  94. package/src/skills/experiment/SKILL.md +1 -1
  95. package/src/skills/finalize/SKILL.md +1 -1
  96. package/src/skills/idea/SKILL.md +1 -1
  97. package/src/skills/intake-audit/SKILL.md +1 -1
  98. package/src/skills/rebuttal/SKILL.md +74 -1
  99. package/src/skills/rebuttal/references/response-letter-template.md +55 -11
  100. package/src/skills/review/SKILL.md +118 -1
  101. package/src/skills/review/references/experiment-todo-template.md +23 -0
  102. package/src/skills/review/references/review-report-template.md +16 -0
  103. package/src/skills/review/references/revision-log-template.md +4 -0
  104. package/src/skills/scout/SKILL.md +1 -1
  105. package/src/skills/write/SKILL.md +168 -7
  106. package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
  107. package/src/tui/package.json +1 -1
  108. package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-CnJcXynW.js} +156 -48
  109. package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
  110. package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-CB1YODQn.js} +164 -9
  111. package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
  112. package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
  113. package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
  114. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -21
  115. package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
  116. package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
  117. package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-Ciz1gDaX.js} +2 -1
  118. package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BhmjNQRC.js} +37 -11
  119. package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
  120. package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-DmyHspXt.js} +3 -3
  121. package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-BMXKrDRk.js} +1 -1
  122. package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BTVYRGkm.js} +12 -12
  123. package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-CvcjJHXv.js} +14 -7
  124. package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DW2ej8Vk.js} +73 -6
  125. package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-CmlDxbhU.js} +103 -34
  126. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  127. package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DAjQZPSv.js} +1 -1
  128. package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C-nVAZb_.js} +5 -4
  129. package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-D7-dIYon.js} +10 -10
  130. package/src/ui/dist/assets/bot-C_G4WtNI.js +21 -0
  131. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  132. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  133. package/src/ui/dist/assets/{code-BWAY76JP.js → code-Cd7WfiWq.js} +1 -1
  134. package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-B57zsL9y.js} +1 -1
  135. package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-DVoheLFq.js} +1 -1
  136. package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-B5kXFxZP.js} +1 -1
  137. package/src/ui/dist/assets/{image-D-NZM-6P.js → image-LLOjkMHF.js} +1 -1
  138. package/src/ui/dist/assets/{index-DGIYDuTv.css → index-BQG-1s2o.css} +40 -13
  139. package/src/ui/dist/assets/{index-DHZJ_0TI.js → index-C3r2iGrp.js} +12 -12
  140. package/src/ui/dist/assets/{index-7Chr1g9c.js → index-CLQauncb.js} +15050 -9561
  141. package/src/ui/dist/assets/index-Dxa2eYMY.js +25 -0
  142. package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-hOUOWbW2.js} +2 -2
  143. package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-BGGAEii3.js} +1 -1
  144. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DlEr1_y5.js} +16 -1
  145. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  146. package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-CWJbJuYY.js} +1 -1
  147. package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-CRJiucYO.js} +18 -77
  148. package/src/ui/dist/assets/select-CoHB7pvH.js +1690 -0
  149. package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-D5aJWR8J.js} +1 -1
  150. package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-DUK_mnkS.js} +2 -13
  151. package/src/ui/dist/assets/{trash-BvTgE5__.js → trash-ChU3SEE3.js} +1 -1
  152. package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-BrJBV3tY.js} +1 -1
  153. package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
  154. package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-C7Qqh-om.js} +1 -1
  155. package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-rtX0FKya.js} +1 -1
  156. package/src/ui/dist/index.html +2 -2
  157. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  158. package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
  159. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  160. package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
  161. package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
  162. package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
  163. package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
  164. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  165. package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
@@ -263,10 +263,7 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
263
263
  ok=False,
264
264
  summary="Codex CLI is not available to DeepScientist.",
265
265
  errors=[f"Runner binary `{binary}` could not be resolved."],
266
- guidance=[
267
- "Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
268
- "Then run `codex` once and complete login.",
269
- ],
266
+ guidance=config_manager._codex_missing_binary_guidance(codex_cfg),
270
267
  details={"binary": binary},
271
268
  )
272
269
 
@@ -285,7 +282,9 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
285
282
  details={"resolved_binary": resolved_binary},
286
283
  )
287
284
  if not probe_guidance:
288
- probe_guidance = ["Run `codex` manually once and complete login, then retry `ds doctor`."]
285
+ probe_guidance = [
286
+ "Run `codex --login` (or `codex`) manually once and complete login, then retry `ds doctor`.",
287
+ ]
289
288
  return _make_check(
290
289
  check_id="codex",
291
290
  label="Codex CLI",
@@ -299,45 +299,41 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
299
299
  continue
300
300
  state = branch_state[branch_name]
301
301
  state.setdefault("branch", branch_name)
302
- state["updated_at"] = record.get("updated_at") or state.get("updated_at")
303
- if record.get("kind") == "run":
304
- state["run_id"] = record.get("run_id") or state.get("run_id")
305
- state["run_kind"] = record.get("run_kind") or state.get("run_kind")
302
+ artifact_sort_key = _artifact_record_sort_key(record, path)
303
+ current_artifact_sort_key = state.get("_latest_artifact_sort_key")
304
+ if current_artifact_sort_key is None or artifact_sort_key > current_artifact_sort_key:
305
+ state["_latest_artifact_sort_key"] = artifact_sort_key
306
+ state["updated_at"] = record.get("updated_at") or record.get("created_at") or state.get("updated_at")
306
307
  if record.get("idea_id"):
307
308
  state["idea_id"] = record.get("idea_id")
308
309
  if record.get("parent_branch"):
309
310
  state["parent_branch"] = record.get("parent_branch")
310
311
  if record.get("worktree_root"):
311
312
  state["worktree_root"] = record.get("worktree_root")
312
- latest_metric = extract_latest_metric(record)
313
+ resolved_run_result = _resolve_run_result_payload(repo, record)
314
+ latest_metric = extract_latest_metric(resolved_run_result if record.get("kind") == "run" else record)
313
315
  if latest_metric is not None:
314
316
  state["latest_metric"] = latest_metric
315
317
  if record.get("kind") == "run":
316
- state["latest_result"] = {
317
- "run_id": record.get("run_id"),
318
- "run_kind": record.get("run_kind"),
319
- "status": record.get("status"),
320
- "summary": record.get("summary") or record.get("reason"),
321
- "verdict": record.get("verdict"),
322
- "paths": record.get("paths") or {},
323
- "details": record.get("details") or {},
324
- "metrics_summary": record.get("metrics_summary") or {},
325
- "metric_rows": record.get("metric_rows") or [],
326
- "metric_contract": record.get("metric_contract") or {},
327
- "baseline_ref": record.get("baseline_ref") or {},
328
- "baseline_comparisons": record.get("baseline_comparisons") or {},
329
- "progress_eval": record.get("progress_eval") or {},
330
- "evaluation_summary": record.get("evaluation_summary")
331
- or ((record.get("details") or {}) if isinstance(record.get("details"), dict) else {}).get("evaluation_summary")
332
- or {},
333
- "files_changed": record.get("files_changed") or [],
334
- "evidence_paths": record.get("evidence_paths") or [],
335
- "updated_at": record.get("updated_at"),
336
- }
337
- state["breakthrough"] = bool(((record.get("progress_eval") or {}).get("breakthrough")))
338
- state["breakthrough_level"] = ((record.get("progress_eval") or {}).get("breakthrough_level"))
318
+ candidate_sort_key = _run_result_sort_key(record, resolved_run_result, path)
319
+ current_sort_key = state.get("_latest_result_sort_key")
320
+ if current_sort_key is None or candidate_sort_key > current_sort_key:
321
+ state["_latest_result_sort_key"] = candidate_sort_key
322
+ state["latest_result"] = resolved_run_result
323
+ state["run_id"] = resolved_run_result.get("run_id") or state.get("run_id")
324
+ state["run_kind"] = resolved_run_result.get("run_kind") or state.get("run_kind")
325
+ progress_eval = (
326
+ resolved_run_result.get("progress_eval")
327
+ if isinstance(resolved_run_result.get("progress_eval"), dict)
328
+ else {}
329
+ )
330
+ state["breakthrough"] = bool(progress_eval.get("breakthrough"))
331
+ state["breakthrough_level"] = progress_eval.get("breakthrough_level")
339
332
  if record.get("summary") or record.get("message") or record.get("reason"):
340
- state["latest_summary"] = record.get("summary") or record.get("message") or record.get("reason")
333
+ current_summary_sort_key = state.get("_latest_summary_sort_key")
334
+ if current_summary_sort_key is None or artifact_sort_key > current_summary_sort_key:
335
+ state["_latest_summary_sort_key"] = artifact_sort_key
336
+ state["latest_summary"] = record.get("summary") or record.get("message") or record.get("reason")
341
337
  state["recent_artifacts"].append(
342
338
  {
343
339
  "artifact_id": record.get("artifact_id"),
@@ -346,12 +342,107 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
346
342
  "reason": record.get("reason"),
347
343
  "updated_at": record.get("updated_at"),
348
344
  "status": record.get("status"),
345
+ "_sort_key": artifact_sort_key,
349
346
  }
350
347
  )
348
+ state["recent_artifacts"].sort(key=lambda item: item.get("_sort_key") or ("", 0, ""))
351
349
  state["recent_artifacts"] = state["recent_artifacts"][-4:]
350
+ for state in branch_state.values():
351
+ latest_result = state.get("latest_result")
352
+ if isinstance(latest_result, dict):
353
+ result_metric = extract_latest_metric(latest_result)
354
+ if result_metric is not None:
355
+ state["latest_metric"] = result_metric
356
+ for item in state.get("recent_artifacts", []):
357
+ if isinstance(item, dict):
358
+ item.pop("_sort_key", None)
352
359
  return branch_state
353
360
 
354
361
 
362
+ def _resolve_run_result_payload(repo: Path, record: dict[str, Any]) -> dict[str, Any]:
363
+ details = dict(record.get("details") or {}) if isinstance(record.get("details"), dict) else {}
364
+ paths = dict(record.get("paths") or {}) if isinstance(record.get("paths"), dict) else {}
365
+ result_payload: dict[str, Any] = {}
366
+ result_json_path = _resolve_result_json_path(repo, paths.get("result_json"))
367
+ if result_json_path and result_json_path.exists():
368
+ loaded = read_json(result_json_path, {})
369
+ if isinstance(loaded, dict):
370
+ result_payload = loaded
371
+
372
+ evaluation_summary = (
373
+ record.get("evaluation_summary")
374
+ or details.get("evaluation_summary")
375
+ or result_payload.get("evaluation_summary")
376
+ or {}
377
+ )
378
+ progress_eval = record.get("progress_eval")
379
+ if not isinstance(progress_eval, dict):
380
+ progress_eval = result_payload.get("progress_eval") if isinstance(result_payload.get("progress_eval"), dict) else {}
381
+
382
+ return {
383
+ "run_id": record.get("run_id") or result_payload.get("run_id"),
384
+ "run_kind": record.get("run_kind") or result_payload.get("run_kind"),
385
+ "status": record.get("status") or result_payload.get("status"),
386
+ "summary": record.get("summary") or record.get("reason") or result_payload.get("summary"),
387
+ "verdict": record.get("verdict") or result_payload.get("verdict"),
388
+ "paths": paths or (result_payload.get("paths") if isinstance(result_payload.get("paths"), dict) else {}) or {},
389
+ "details": details,
390
+ "metrics_summary": record.get("metrics_summary") or result_payload.get("metrics_summary") or {},
391
+ "metric_rows": record.get("metric_rows") or result_payload.get("metric_rows") or [],
392
+ "metric_contract": record.get("metric_contract") or result_payload.get("metric_contract") or {},
393
+ "baseline_ref": record.get("baseline_ref") or result_payload.get("baseline_ref") or {},
394
+ "baseline_comparisons": record.get("baseline_comparisons") or result_payload.get("baseline_comparisons") or {},
395
+ "progress_eval": progress_eval or {},
396
+ "evaluation_summary": evaluation_summary or {},
397
+ "files_changed": record.get("files_changed") or result_payload.get("files_changed") or [],
398
+ "evidence_paths": record.get("evidence_paths") or result_payload.get("evidence_paths") or [],
399
+ "updated_at": record.get("updated_at") or result_payload.get("updated_at"),
400
+ }
401
+
402
+
403
+ def _resolve_result_json_path(repo: Path, raw_path: object) -> Path | None:
404
+ normalized = str(raw_path or "").strip()
405
+ if not normalized:
406
+ return None
407
+ candidate = Path(normalized).expanduser()
408
+ if candidate.is_absolute():
409
+ return candidate
410
+ return repo / candidate
411
+
412
+
413
+ def _artifact_record_sort_key(record: dict[str, Any], path: Path) -> tuple[str, int, str]:
414
+ updated_at = str(record.get("updated_at") or record.get("created_at") or "").strip()
415
+ try:
416
+ mtime_ns = path.stat().st_mtime_ns
417
+ except OSError:
418
+ mtime_ns = 0
419
+ return (updated_at, mtime_ns, str(path))
420
+
421
+
422
+ def _run_result_sort_key(record: dict[str, Any], payload: dict[str, Any], path: Path) -> tuple[int, str, int, str]:
423
+ quality = 0
424
+ if extract_latest_metric(payload):
425
+ quality += 8
426
+ if payload.get("baseline_comparisons"):
427
+ quality += 4
428
+ if payload.get("progress_eval"):
429
+ quality += 4
430
+ if payload.get("metrics_summary"):
431
+ quality += 3
432
+ if payload.get("metric_rows"):
433
+ quality += 3
434
+ if payload.get("verdict"):
435
+ quality += 2
436
+ paths = payload.get("paths") if isinstance(payload.get("paths"), dict) else {}
437
+ if paths.get("result_json"):
438
+ quality += 2
439
+ if paths.get("run_md"):
440
+ quality += 1
441
+ updated_at = str(payload.get("updated_at") or record.get("updated_at") or record.get("created_at") or "")
442
+ _, mtime_ns, path_str = _artifact_record_sort_key(record, path)
443
+ return (quality, updated_at, mtime_ns, path_str)
444
+
445
+
355
446
  def _classify_ref(ref: str, state: dict[str, Any]) -> dict[str, str]:
356
447
  run_id = str(state.get("run_id") or "")
357
448
  run_kind = str(state.get("run_kind") or "")
@@ -1,182 +1 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import secrets
5
- from typing import Any
6
- from urllib.parse import urlparse
7
-
8
-
9
- DEFAULT_LINGZHU_GATEWAY_PORT = 18789
10
- DEFAULT_LINGZHU_LOCAL_HOST = "127.0.0.1"
11
- DEFAULT_LINGZHU_AGENT_ID = "main"
12
- DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
13
-
14
- _AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
15
- _AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
16
-
17
-
18
- def generate_lingzhu_auth_ak() -> str:
19
- parts: list[str] = []
20
- for segment_length in _AUTH_AK_SEGMENTS:
21
- parts.append("".join(secrets.choice(_AUTH_AK_CHARS) for _ in range(segment_length)))
22
- return "-".join(parts)
23
-
24
-
25
- def lingzhu_local_host(config: dict[str, Any] | None) -> str:
26
- value = str((config or {}).get("local_host") or DEFAULT_LINGZHU_LOCAL_HOST).strip()
27
- return value or DEFAULT_LINGZHU_LOCAL_HOST
28
-
29
-
30
- def lingzhu_gateway_port(config: dict[str, Any] | None) -> int:
31
- raw = (config or {}).get("gateway_port")
32
- try:
33
- value = int(raw)
34
- except (TypeError, ValueError):
35
- return DEFAULT_LINGZHU_GATEWAY_PORT
36
- if value < 1 or value > 65535:
37
- return DEFAULT_LINGZHU_GATEWAY_PORT
38
- return value
39
-
40
-
41
- def normalize_public_base_url(value: Any) -> str | None:
42
- text = str(value or "").strip()
43
- if not text:
44
- return None
45
- parsed = urlparse(text)
46
- if parsed.scheme not in {"http", "https"} or not parsed.netloc:
47
- return None
48
- return text.rstrip("/")
49
-
50
-
51
- def lingzhu_local_base_url(config: dict[str, Any] | None) -> str:
52
- return f"http://{lingzhu_local_host(config)}:{lingzhu_gateway_port(config)}"
53
-
54
-
55
- def lingzhu_public_base_url(config: dict[str, Any] | None) -> str | None:
56
- return normalize_public_base_url((config or {}).get("public_base_url"))
57
-
58
-
59
- def lingzhu_health_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
60
- base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
61
- if not base:
62
- return None
63
- return f"{base}/metis/agent/api/health"
64
-
65
-
66
- def lingzhu_sse_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
67
- base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
68
- if not base:
69
- return None
70
- return f"{base}/metis/agent/api/sse"
71
-
72
-
73
- def lingzhu_agent_id(config: dict[str, Any] | None) -> str:
74
- value = str((config or {}).get("agent_id") or DEFAULT_LINGZHU_AGENT_ID).strip()
75
- return value or DEFAULT_LINGZHU_AGENT_ID
76
-
77
-
78
- def lingzhu_probe_payload(
79
- config: dict[str, Any] | None,
80
- *,
81
- message_id: str = "ds-lingzhu-probe-001",
82
- text: str = "你好",
83
- ) -> dict[str, Any]:
84
- return {
85
- "message_id": message_id,
86
- "agent_id": lingzhu_agent_id(config),
87
- "message": [
88
- {
89
- "role": "user",
90
- "type": "text",
91
- "text": text,
92
- }
93
- ],
94
- }
95
-
96
-
97
- def lingzhu_generated_openclaw_config(config: dict[str, Any] | None) -> dict[str, Any]:
98
- resolved = dict(config or {})
99
- return {
100
- "gateway": {
101
- "port": lingzhu_gateway_port(resolved),
102
- "http": {
103
- "endpoints": {
104
- "chatCompletions": {
105
- "enabled": True,
106
- }
107
- }
108
- },
109
- },
110
- "plugins": {
111
- "entries": {
112
- "lingzhu": {
113
- "enabled": bool(resolved.get("enabled", False)),
114
- "config": {
115
- "authAk": str(resolved.get("auth_ak") or "").strip(),
116
- "agentId": lingzhu_agent_id(resolved),
117
- "includeMetadata": bool(resolved.get("include_metadata", True)),
118
- "requestTimeoutMs": int(resolved.get("request_timeout_ms") or 60000),
119
- "systemPrompt": str(resolved.get("system_prompt") or ""),
120
- "defaultNavigationMode": str(resolved.get("default_navigation_mode") or "0"),
121
- "enableFollowUp": bool(resolved.get("enable_follow_up", True)),
122
- "followUpMaxCount": int(resolved.get("follow_up_max_count") or 3),
123
- "maxImageBytes": int(resolved.get("max_image_bytes") or 5 * 1024 * 1024),
124
- "sessionMode": str(resolved.get("session_mode") or "per_user"),
125
- "sessionNamespace": str(
126
- resolved.get("session_namespace") or DEFAULT_LINGZHU_SESSION_NAMESPACE
127
- ),
128
- "autoReceiptAck": bool(resolved.get("auto_receipt_ack", True)),
129
- "visibleProgressHeartbeat": bool(
130
- resolved.get("visible_progress_heartbeat", True)
131
- ),
132
- "visibleProgressHeartbeatSec": int(
133
- resolved.get("visible_progress_heartbeat_sec") or 10
134
- ),
135
- "debugLogging": bool(resolved.get("debug_logging", False)),
136
- "debugLogPayloads": bool(resolved.get("debug_log_payloads", False)),
137
- "debugLogDir": str(resolved.get("debug_log_dir") or ""),
138
- "enableExperimentalNativeActions": bool(
139
- resolved.get("enable_experimental_native_actions", False)
140
- ),
141
- },
142
- }
143
- }
144
- },
145
- }
146
-
147
-
148
- def lingzhu_generated_openclaw_config_text(config: dict[str, Any] | None) -> str:
149
- return json.dumps(lingzhu_generated_openclaw_config(config), indent=2, ensure_ascii=False)
150
-
151
-
152
- def lingzhu_generated_curl(config: dict[str, Any] | None, *, text: str = "你好") -> str:
153
- auth_ak = str((config or {}).get("auth_ak") or "").strip()
154
- payload = lingzhu_probe_payload(config, text=text)
155
- endpoint_url = lingzhu_sse_url(config) or ""
156
- return (
157
- f"curl -X POST '{endpoint_url}' \\\n"
158
- f" --header 'Authorization: Bearer {auth_ak}' \\\n"
159
- " --header 'Content-Type: application/json' \\\n"
160
- f" --data '{json.dumps(payload, ensure_ascii=False)}'"
161
- )
162
-
163
-
164
- def lingzhu_supported_commands(*, experimental_enabled: bool) -> list[str]:
165
- commands = [
166
- "take_photo",
167
- "take_navigation",
168
- "control_calendar",
169
- "notify_agent_off",
170
- ]
171
- if experimental_enabled:
172
- commands.extend(
173
- [
174
- "send_notification",
175
- "send_toast",
176
- "speak_tts",
177
- "start_video_record",
178
- "stop_video_record",
179
- "open_custom_view",
180
- ]
181
- )
182
- return commands
1
+ from .connector.lingzhu_support import * # noqa: F401,F403
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ from collections import deque
5
+ from pathlib import Path
4
6
  from typing import Any
5
7
 
6
8
  from mcp.server.fastmcp import FastMCP
@@ -95,6 +97,125 @@ def _build_default_bash_log_payload(log_text: str) -> dict[str, Any]:
95
97
  }
96
98
 
97
99
 
100
+ def _stream_bash_log_summary(path: Path) -> tuple[list[str], int, list[str]]:
101
+ total = 0
102
+ full_lines: list[str] = []
103
+ head_lines: list[str] = []
104
+ tail_lines: deque[str] = deque(maxlen=DEFAULT_INLINE_BASH_LOG_TAIL_LINES)
105
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
106
+ for raw_line in handle:
107
+ line = raw_line.rstrip("\n")
108
+ total += 1
109
+ if total <= DEFAULT_INLINE_BASH_LOG_LINE_LIMIT:
110
+ full_lines.append(line)
111
+ continue
112
+ if total == DEFAULT_INLINE_BASH_LOG_LINE_LIMIT + 1:
113
+ head_lines = full_lines[:DEFAULT_INLINE_BASH_LOG_HEAD_LINES]
114
+ tail_lines.extend(full_lines[-DEFAULT_INLINE_BASH_LOG_TAIL_LINES :])
115
+ full_lines = []
116
+ tail_lines.append(line)
117
+ return full_lines, total, list(head_lines or tail_lines)
118
+
119
+
120
+ def _build_default_bash_log_payload_from_path(path: Path) -> dict[str, Any]:
121
+ if not path.exists():
122
+ return {
123
+ "log": "",
124
+ "log_line_count": 0,
125
+ "log_truncated": False,
126
+ }
127
+ full_lines, total, preview_seed = _stream_bash_log_summary(path)
128
+ if total <= DEFAULT_INLINE_BASH_LOG_LINE_LIMIT:
129
+ return {
130
+ "log": _join_bash_log_lines(full_lines),
131
+ "log_line_count": total,
132
+ "log_truncated": False,
133
+ }
134
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
135
+ tail_lines: deque[str] = deque(maxlen=DEFAULT_INLINE_BASH_LOG_TAIL_LINES)
136
+ for raw_line in handle:
137
+ tail_lines.append(raw_line.rstrip("\n"))
138
+ omitted = total - DEFAULT_INLINE_BASH_LOG_HEAD_LINES - DEFAULT_INLINE_BASH_LOG_TAIL_LINES
139
+ marker = (
140
+ f"[... omitted {omitted} lines from the middle of this log. {LONG_BASH_LOG_HINT}]"
141
+ )
142
+ preview_lines = preview_seed[:DEFAULT_INLINE_BASH_LOG_HEAD_LINES] + [marker] + list(tail_lines)
143
+ return {
144
+ "log": _join_bash_log_lines(preview_lines),
145
+ "log_line_count": total,
146
+ "log_truncated": True,
147
+ "log_preview_head_lines": DEFAULT_INLINE_BASH_LOG_HEAD_LINES,
148
+ "log_preview_tail_lines": DEFAULT_INLINE_BASH_LOG_TAIL_LINES,
149
+ "log_preview_omitted_lines": omitted,
150
+ "log_read_hint": LONG_BASH_LOG_HINT,
151
+ }
152
+
153
+
154
+ def _build_bash_log_window_from_path(path: Path, *, start: int | None = None, tail: int | None = None) -> dict[str, Any]:
155
+ if not path.exists():
156
+ return {
157
+ "log": "",
158
+ "log_line_count": 0,
159
+ "log_windowed": True,
160
+ "line_start": 1,
161
+ "line_end": 0,
162
+ "line_limit": _normalize_bash_log_window_size(tail),
163
+ "returned_line_count": 0,
164
+ "has_more_before": False,
165
+ "has_more_after": False,
166
+ "log_read_hint": LONG_BASH_LOG_HINT,
167
+ }
168
+ line_limit = _normalize_bash_log_window_size(tail)
169
+ if start is not None:
170
+ requested_start = max(1, int(start))
171
+ selected: list[str] = []
172
+ total = 0
173
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
174
+ for raw_line in handle:
175
+ total += 1
176
+ if total < requested_start:
177
+ continue
178
+ if len(selected) < line_limit:
179
+ selected.append(raw_line.rstrip("\n"))
180
+ returned_count = len(selected)
181
+ line_start = requested_start if total else 1
182
+ line_end = requested_start + returned_count - 1 if returned_count else requested_start - 1
183
+ return {
184
+ "log": _join_bash_log_lines(selected),
185
+ "log_line_count": total,
186
+ "log_windowed": True,
187
+ "line_start": line_start,
188
+ "line_end": line_end,
189
+ "line_limit": line_limit,
190
+ "returned_line_count": returned_count,
191
+ "has_more_before": line_start > 1,
192
+ "has_more_after": line_end < total,
193
+ "log_read_hint": LONG_BASH_LOG_HINT,
194
+ }
195
+
196
+ tail_lines: deque[str] = deque(maxlen=line_limit)
197
+ total = 0
198
+ with path.open("r", encoding="utf-8", errors="replace") as handle:
199
+ for raw_line in handle:
200
+ total += 1
201
+ tail_lines.append(raw_line.rstrip("\n"))
202
+ returned_count = len(tail_lines)
203
+ line_start = max(1, total - returned_count + 1) if total else 1
204
+ line_end = total
205
+ return {
206
+ "log": _join_bash_log_lines(list(tail_lines)),
207
+ "log_line_count": total,
208
+ "log_windowed": True,
209
+ "line_start": line_start,
210
+ "line_end": line_end,
211
+ "line_limit": line_limit,
212
+ "returned_line_count": returned_count,
213
+ "has_more_before": line_start > 1,
214
+ "has_more_after": False,
215
+ "log_read_hint": LONG_BASH_LOG_HINT,
216
+ }
217
+
218
+
98
219
  def build_memory_server(context: McpContext) -> FastMCP:
99
220
  service = MemoryService(context.home)
100
221
  server = FastMCP(
@@ -680,16 +801,23 @@ def build_artifact_server(context: McpContext) -> FastMCP:
680
801
  @server.tool(
681
802
  name="arxiv",
682
803
  description=(
683
- "Read an identified arXiv paper by id. "
684
- "Use full_text=false for the overview/abstract path and full_text=true when the full paper body is needed."
804
+ "Interact with the quest-local arXiv library. "
805
+ "Use mode='read' to read one paper by id with local-first automatic persistence, "
806
+ "or mode='list' to list the saved arXiv items for the current quest."
685
807
  ),
686
808
  )
687
809
  def arxiv(
688
- paper_id: str,
810
+ paper_id: str | None = None,
811
+ mode: str = "read",
689
812
  full_text: bool = False,
690
813
  comment: str | dict[str, Any] | None = None,
691
814
  ) -> dict[str, Any]:
692
- return service.arxiv(paper_id, full_text=full_text)
815
+ return service.arxiv(
816
+ paper_id,
817
+ mode=mode,
818
+ full_text=full_text,
819
+ quest_root=context.require_quest_root(),
820
+ )
693
821
 
694
822
  @server.tool(name="refresh_summary", description="Refresh SUMMARY.md from recent artifact state.")
695
823
  def refresh_summary(
@@ -873,8 +1001,8 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
873
1001
  export_log_to=export_log_to,
874
1002
  )
875
1003
  payload.update(
876
- _build_bash_log_window(
877
- service.read_terminal_log(quest_root, bash_id),
1004
+ _build_bash_log_window_from_path(
1005
+ service.terminal_log_path(quest_root, bash_id),
878
1006
  start=start,
879
1007
  tail=tail if tail is not None else tail_limit,
880
1008
  )
@@ -914,7 +1042,7 @@ def build_bash_exec_server(context: McpContext) -> FastMCP:
914
1042
  export_log=export_log,
915
1043
  export_log_to=export_log_to,
916
1044
  )
917
- payload.update(_build_default_bash_log_payload(service.read_terminal_log(quest_root, bash_id)))
1045
+ payload.update(_build_default_bash_log_payload_from_path(service.terminal_log_path(quest_root, bash_id)))
918
1046
  return payload
919
1047
  if normalized_mode == "kill":
920
1048
  bash_id = service.resolve_session_id(quest_root, id)