@researai/deepscientist 1.5.8 → 1.5.11

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 (148) hide show
  1. package/LICENSE +186 -21
  2. package/README.md +108 -95
  3. package/assets/branding/connector-qq.png +0 -0
  4. package/assets/branding/connector-rokid.png +0 -0
  5. package/assets/branding/connector-weixin.png +0 -0
  6. package/assets/branding/projects.png +0 -0
  7. package/bin/ds.js +172 -13
  8. package/docs/assets/branding/projects.png +0 -0
  9. package/docs/en/00_QUICK_START.md +308 -70
  10. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  11. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  12. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  13. package/docs/en/09_DOCTOR.md +41 -5
  14. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  15. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  16. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
  17. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  18. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  19. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  20. package/docs/en/README.md +79 -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 +315 -74
  28. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  29. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  30. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  31. package/docs/zh/09_DOCTOR.md +41 -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 +423 -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/99_ACKNOWLEDGEMENTS.md +4 -1
  38. package/docs/zh/README.md +126 -0
  39. package/install.sh +0 -34
  40. package/package.json +3 -3
  41. package/pyproject.toml +2 -2
  42. package/src/deepscientist/__init__.py +1 -1
  43. package/src/deepscientist/annotations.py +343 -0
  44. package/src/deepscientist/artifact/arxiv.py +484 -37
  45. package/src/deepscientist/artifact/metrics.py +1 -3
  46. package/src/deepscientist/artifact/service.py +1347 -111
  47. package/src/deepscientist/arxiv_library.py +275 -0
  48. package/src/deepscientist/bash_exec/service.py +9 -0
  49. package/src/deepscientist/bridges/builtins.py +2 -0
  50. package/src/deepscientist/bridges/connectors.py +447 -0
  51. package/src/deepscientist/channels/__init__.py +2 -0
  52. package/src/deepscientist/channels/builtins.py +3 -1
  53. package/src/deepscientist/channels/qq.py +1 -1
  54. package/src/deepscientist/channels/qq_gateway.py +1 -1
  55. package/src/deepscientist/channels/relay.py +7 -1
  56. package/src/deepscientist/channels/weixin.py +59 -0
  57. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  58. package/src/deepscientist/config/models.py +22 -2
  59. package/src/deepscientist/config/service.py +431 -60
  60. package/src/deepscientist/connector/__init__.py +4 -0
  61. package/src/deepscientist/connector/connector_profiles.py +481 -0
  62. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  63. package/src/deepscientist/connector/qq_profiles.py +206 -0
  64. package/src/deepscientist/connector/weixin_support.py +663 -0
  65. package/src/deepscientist/connector_profiles.py +1 -374
  66. package/src/deepscientist/connector_runtime.py +2 -0
  67. package/src/deepscientist/daemon/api/handlers.py +295 -5
  68. package/src/deepscientist/daemon/api/router.py +16 -1
  69. package/src/deepscientist/daemon/app.py +1130 -61
  70. package/src/deepscientist/doctor.py +5 -2
  71. package/src/deepscientist/gitops/diff.py +120 -29
  72. package/src/deepscientist/lingzhu_support.py +1 -182
  73. package/src/deepscientist/mcp/server.py +14 -5
  74. package/src/deepscientist/prompts/builder.py +29 -1
  75. package/src/deepscientist/qq_profiles.py +1 -196
  76. package/src/deepscientist/quest/node_traces.py +152 -2
  77. package/src/deepscientist/quest/service.py +169 -43
  78. package/src/deepscientist/quest/stage_views.py +172 -9
  79. package/src/deepscientist/registries/baseline.py +56 -4
  80. package/src/deepscientist/runners/codex.py +55 -3
  81. package/src/deepscientist/weixin_support.py +1 -0
  82. package/src/prompts/connectors/lingzhu.md +3 -1
  83. package/src/prompts/connectors/weixin.md +230 -0
  84. package/src/prompts/system.md +9 -0
  85. package/src/skills/idea/SKILL.md +16 -0
  86. package/src/skills/idea/references/literature-survey-template.md +24 -0
  87. package/src/skills/idea/references/related-work-playbook.md +4 -0
  88. package/src/skills/idea/references/selection-gate.md +9 -0
  89. package/src/skills/write/SKILL.md +1 -1
  90. package/src/tui/package.json +1 -1
  91. package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  92. package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  93. package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
  94. package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  95. package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  96. package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  97. package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  98. package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  99. package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
  100. package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
  101. package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
  102. package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  103. package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
  104. package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
  105. package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  106. package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
  107. package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  108. package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  109. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  110. package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
  111. package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  112. package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
  113. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  114. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  115. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  116. package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
  117. package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
  118. package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
  119. package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
  120. package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
  121. package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
  122. package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
  123. package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
  124. package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
  125. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  126. package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
  127. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  128. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  129. package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
  130. package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
  131. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  132. package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
  133. package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
  134. package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
  135. package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
  136. package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  137. package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
  138. package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
  139. package/src/ui/dist/index.html +2 -2
  140. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  141. package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
  142. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  143. package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
  144. package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
  145. package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
  146. package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
  147. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  148. package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
@@ -265,7 +265,8 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
265
265
  errors=[f"Runner binary `{binary}` could not be resolved."],
266
266
  guidance=[
267
267
  "Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
268
- "Then run `codex` once and complete login.",
268
+ "If `codex` is still missing, install it explicitly with `npm install -g @openai/codex`.",
269
+ "Then run `codex --login` (or `codex`) once and complete login.",
269
270
  ],
270
271
  details={"binary": binary},
271
272
  )
@@ -285,7 +286,9 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
285
286
  details={"resolved_binary": resolved_binary},
286
287
  )
287
288
  if not probe_guidance:
288
- probe_guidance = ["Run `codex` manually once and complete login, then retry `ds doctor`."]
289
+ probe_guidance = [
290
+ "Run `codex --login` (or `codex`) manually once and complete login, then retry `ds doctor`.",
291
+ ]
289
292
  return _make_check(
290
293
  check_id="codex",
291
294
  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
@@ -471,7 +471,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
471
471
  context.require_quest_root(),
472
472
  campaign_title=campaign_title,
473
473
  campaign_goal=campaign_goal,
474
- parent_run_id=parent_run_id or context.run_id,
474
+ parent_run_id=parent_run_id,
475
475
  slices=slices,
476
476
  campaign_origin=campaign_origin,
477
477
  selected_outline_ref=selected_outline_ref,
@@ -636,6 +636,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
636
636
  summary: str | None = None,
637
637
  baseline_kind: str | None = None,
638
638
  metric_contract: dict[str, Any] | None = None,
639
+ metric_directions: dict[str, str] | None = None,
639
640
  metrics_summary: dict[str, Any] | None = None,
640
641
  primary_metric: dict[str, Any] | None = None,
641
642
  auto_advance: bool = True,
@@ -651,6 +652,7 @@ def build_artifact_server(context: McpContext) -> FastMCP:
651
652
  summary=summary,
652
653
  baseline_kind=baseline_kind,
653
654
  metric_contract=metric_contract,
655
+ metric_directions=metric_directions,
654
656
  metrics_summary=metrics_summary,
655
657
  primary_metric=primary_metric,
656
658
  auto_advance=auto_advance,
@@ -678,16 +680,23 @@ def build_artifact_server(context: McpContext) -> FastMCP:
678
680
  @server.tool(
679
681
  name="arxiv",
680
682
  description=(
681
- "Read an identified arXiv paper by id. "
682
- "Use full_text=false for the overview/abstract path and full_text=true when the full paper body is needed."
683
+ "Interact with the quest-local arXiv library. "
684
+ "Use mode='read' to read one paper by id with local-first automatic persistence, "
685
+ "or mode='list' to list the saved arXiv items for the current quest."
683
686
  ),
684
687
  )
685
688
  def arxiv(
686
- paper_id: str,
689
+ paper_id: str | None = None,
690
+ mode: str = "read",
687
691
  full_text: bool = False,
688
692
  comment: str | dict[str, Any] | None = None,
689
693
  ) -> dict[str, Any]:
690
- return service.arxiv(paper_id, full_text=full_text)
694
+ return service.arxiv(
695
+ paper_id,
696
+ mode=mode,
697
+ full_text=full_text,
698
+ quest_root=context.require_quest_root(),
699
+ )
691
700
 
692
701
  @server.tool(name="refresh_summary", description="Refresh SUMMARY.md from recent artifact state.")
693
702
  def refresh_summary(
@@ -283,6 +283,21 @@ class PromptBuilder:
283
283
  "- qq_structured_delivery_rule: when you want native QQ markdown or native QQ image/file delivery, request it through artifact.interact(connector_hints=..., attachments=[...]) instead of inventing connector-specific inline tag syntax.",
284
284
  ]
285
285
  )
286
+ elif connector == "weixin":
287
+ lines.extend(
288
+ [
289
+ "- weixin_surface_rule: Weixin is a concise operator surface, not a full artifact browser.",
290
+ "- weixin_default_mode: keep outbound replies concise, respectful, text-first, and progress-aware.",
291
+ "- weixin_length_rule: for ordinary Weixin progress replies, normally use only 2 to 4 short sentences, or 3 very short bullets at most.",
292
+ "- weixin_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action.",
293
+ "- weixin_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete next step explicit whenever possible.",
294
+ "- weixin_eta_rule: for important long-running phases, include a rough ETA or next check-in window when it is helpful and defensible.",
295
+ "- weixin_internal_detail_rule: do not proactively dump file inventories, path lists, retry counters, or monitor-log style telemetry unless the user asked for them or they explain a real risk.",
296
+ "- weixin_context_token_rule: reply continuity is managed by the runtime through `context_token`; do not invent your own reply token scheme.",
297
+ "- weixin_media_rule: when you want native Weixin image, video, or file delivery, request it through artifact.interact(..., attachments=[...]) with `connector_delivery={'weixin': {'media_kind': ...}}` instead of inventing connector-specific inline tag syntax.",
298
+ "- weixin_inbound_media_rule: inbound Weixin image, video, and file messages can arrive as quest-local attachments under `userfiles/weixin/...`; read those files when the user sent media.",
299
+ ]
300
+ )
286
301
  else:
287
302
  lines.append("- connector_media_rule: if the active surface is not QQ, keep using the general artifact interaction discipline for milestone delivery.")
288
303
 
@@ -687,6 +702,8 @@ class PromptBuilder:
687
702
  f"- delivery_mode: {'paper_required' if need_research_paper else 'algorithm_first'}",
688
703
  "- idea_stage_rule: every accepted idea submission should normally create a new branch/worktree and a new user-visible research node.",
689
704
  "- idea_draft_rule: before `artifact.submit_idea(...)`, first finish a concise durable Markdown draft for the chosen route; keep `idea.md` compact and `draft.md` richer.",
705
+ "- idea_literature_floor_rule: before writing or submitting a final selected idea, durably survey at least 5 and usually 5 to 10 related and usable papers; prioritize direct task-modeling or mechanism-neighbor work and only backfill with the closest adjacent translatable papers when necessary.",
706
+ "- idea_reference_rule: the final selected-idea draft should use one consistent standard citation format and include a `References` or `Bibliography` section for the survey-stage papers that actually shaped the motivation, mechanism, or claim boundary.",
690
707
  "- lineage_rule: normal idea routing uses exactly two lineage intents: `continue_line` creates a child of the current active branch; `branch_alternative` creates a sibling-like branch from the current branch's parent foundation.",
691
708
  "- revise_rule: `artifact.submit_idea(mode='revise', ...)` is maintenance-only compatibility for the same branch and should not be the default research-route mechanism.",
692
709
  "- post_main_result_rule: after every `artifact.record_main_experiment(...)`, first interpret the measured result and only then choose the next route.",
@@ -806,6 +823,8 @@ class PromptBuilder:
806
823
  "- idea_why_now_protocol: every serious idea candidate should answer why now or what changed, not just what the mechanism is",
807
824
  "- idea_balance_protocol: when the search space is not tiny, carry at least one conservative route and one higher-upside route into the final comparison",
808
825
  "- idea_pitch_protocol: before artifact.submit_idea(...), make the winner pass a two-sentence pitch, a strongest-objection check, and a concrete why-now statement",
826
+ "- idea_literature_floor_protocol: do not write or submit the final selected idea until the durable survey covers at least 5 and usually 5 to 10 related and usable papers; if fewer than 5 direct papers exist, document the shortage and use the closest adjacent translatable work instead of skipping the gate",
827
+ "- idea_reference_protocol: the final selected-idea draft should cite the survey-stage papers it actually uses and end with a standard-format `References` or `Bibliography` section",
809
828
  "- experiment_milestone_protocol: immediately after artifact.record_main_experiment(...) writes the durable result, send a threaded milestone that explains what was run, the main result, whether primary performance improved / worsened / stayed mixed versus the active baseline or best prior anchor, whether the route still looks promising, and the exact next step",
810
829
  "- analysis_milestone_protocol: immediately after a meaningful completed analysis-campaign synthesis or route-significant campaign checkpoint, send a threaded milestone that explains which campaign question or slice set just closed, whether the claim boundary became stronger / weaker / mixed, the main caveat, and the exact next route",
811
830
  "- paper_milestone_protocol: immediately after a meaningful paper or draft milestone such as selected outline, evidence-complete draft, major revision package, or bundle-ready paper, send a threaded milestone that explains what document milestone is now complete, which claims are now supportable, what still needs strengthening, and the exact next revision or execution route",
@@ -952,7 +971,16 @@ class PromptBuilder:
952
971
  attachment_root = active_workspace_root / "baselines" / "imported"
953
972
  if attachment_root.exists():
954
973
  attachments = [read_yaml(path, {}) for path in sorted(attachment_root.glob("*/attachment.yaml"))]
955
- attachments = [item for item in attachments if isinstance(item, dict) and item]
974
+ attachments = [
975
+ item
976
+ for item in attachments
977
+ if isinstance(item, dict)
978
+ and item
979
+ and (
980
+ not str(item.get("source_baseline_id") or "").strip()
981
+ or not self.baseline_registry.is_deleted(str(item.get("source_baseline_id") or "").strip())
982
+ )
983
+ ]
956
984
  if attachments:
957
985
  attachment = max(
958
986
  attachments,