@researai/deepscientist 1.5.9 → 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 (140) hide show
  1. package/README.md +107 -94
  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 +168 -9
  7. package/docs/assets/branding/projects.png +0 -0
  8. package/docs/en/00_QUICK_START.md +308 -70
  9. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  10. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  11. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  12. package/docs/en/09_DOCTOR.md +41 -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 +427 -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/99_ACKNOWLEDGEMENTS.md +4 -1
  19. package/docs/en/README.md +79 -0
  20. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  21. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  23. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  24. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  25. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  26. package/docs/zh/00_QUICK_START.md +315 -74
  27. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  28. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  29. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  30. package/docs/zh/09_DOCTOR.md +41 -5
  31. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  32. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  33. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
  34. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  35. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  36. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  37. package/docs/zh/README.md +126 -0
  38. package/install.sh +0 -34
  39. package/package.json +2 -2
  40. package/pyproject.toml +1 -1
  41. package/src/deepscientist/__init__.py +1 -1
  42. package/src/deepscientist/annotations.py +343 -0
  43. package/src/deepscientist/artifact/arxiv.py +484 -37
  44. package/src/deepscientist/artifact/service.py +574 -108
  45. package/src/deepscientist/arxiv_library.py +275 -0
  46. package/src/deepscientist/bash_exec/service.py +9 -0
  47. package/src/deepscientist/bridges/builtins.py +2 -0
  48. package/src/deepscientist/bridges/connectors.py +447 -0
  49. package/src/deepscientist/channels/__init__.py +2 -0
  50. package/src/deepscientist/channels/builtins.py +3 -1
  51. package/src/deepscientist/channels/qq.py +1 -1
  52. package/src/deepscientist/channels/qq_gateway.py +1 -1
  53. package/src/deepscientist/channels/relay.py +7 -1
  54. package/src/deepscientist/channels/weixin.py +59 -0
  55. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  56. package/src/deepscientist/config/models.py +22 -2
  57. package/src/deepscientist/config/service.py +431 -60
  58. package/src/deepscientist/connector/__init__.py +4 -0
  59. package/src/deepscientist/connector/connector_profiles.py +481 -0
  60. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  61. package/src/deepscientist/connector/qq_profiles.py +206 -0
  62. package/src/deepscientist/connector/weixin_support.py +663 -0
  63. package/src/deepscientist/connector_profiles.py +1 -374
  64. package/src/deepscientist/connector_runtime.py +2 -0
  65. package/src/deepscientist/daemon/api/handlers.py +165 -5
  66. package/src/deepscientist/daemon/api/router.py +13 -1
  67. package/src/deepscientist/daemon/app.py +1130 -61
  68. package/src/deepscientist/doctor.py +5 -2
  69. package/src/deepscientist/gitops/diff.py +120 -29
  70. package/src/deepscientist/lingzhu_support.py +1 -182
  71. package/src/deepscientist/mcp/server.py +11 -4
  72. package/src/deepscientist/prompts/builder.py +15 -0
  73. package/src/deepscientist/qq_profiles.py +1 -196
  74. package/src/deepscientist/quest/node_traces.py +23 -0
  75. package/src/deepscientist/quest/service.py +112 -43
  76. package/src/deepscientist/quest/stage_views.py +71 -5
  77. package/src/deepscientist/runners/codex.py +55 -3
  78. package/src/deepscientist/weixin_support.py +1 -0
  79. package/src/prompts/connectors/lingzhu.md +3 -1
  80. package/src/prompts/connectors/weixin.md +230 -0
  81. package/src/prompts/system.md +2 -0
  82. package/src/tui/package.json +1 -1
  83. package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  84. package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  85. package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-DrV8je02.js} +164 -9
  86. package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  87. package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  88. package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  89. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  90. package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  91. package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-1qSow1es.js} +11 -11
  92. package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-eQpPPCEp.js} +2 -1
  93. package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BwRfi89Z.js} +7 -7
  94. package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  95. package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-C2y_556i.js} +3 -3
  96. package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BRzJbGsn.js} +12 -12
  97. package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  98. package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-DzRaTAlq.js} +14 -7
  99. package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  100. package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  101. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  102. package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DHeIAMsx.js} +1 -1
  103. package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  104. package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-CQsKVm3t.js} +10 -10
  105. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  106. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  107. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  108. package/src/ui/dist/assets/{code-BWAY76JP.js → code-XfbSR8K2.js} +1 -1
  109. package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-BjxNaIfy.js} +1 -1
  110. package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-D_lLVQk0.js} +1 -1
  111. package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-D9x_5vlY.js} +1 -1
  112. package/src/ui/dist/assets/{image-D-NZM-6P.js → image-BhWT33W1.js} +1 -1
  113. package/src/ui/dist/assets/{index-DHZJ_0TI.js → index--c4iXtuy.js} +12 -12
  114. package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-BDxipwrC.js} +2 -2
  115. package/src/ui/dist/assets/{index-7Chr1g9c.js → index-DZTZ8mWP.js} +14221 -9523
  116. package/src/ui/dist/assets/{index-DGIYDuTv.css → index-Dqj-Mjb4.css} +2 -13
  117. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  118. package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-K8izTGgo.js} +1 -1
  119. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  120. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  121. package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-yFK1J4fL.js} +1 -1
  122. package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-PENr2zcz.js} +1 -74
  123. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  124. package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-DEuYJqTl.js} +1 -1
  125. package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-omoSUmcd.js} +2 -13
  126. package/src/ui/dist/assets/{trash-BvTgE5__.js → trash--F119N47.js} +1 -1
  127. package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-D31UR23I.js} +1 -1
  128. package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  129. package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-CZ613PM5.js} +1 -1
  130. package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-BgDLAv3z.js} +1 -1
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  133. package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
  134. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  135. package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
  136. package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
  137. package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
  138. package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
  139. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  140. package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
@@ -6,6 +6,8 @@ from typing import Any
6
6
 
7
7
  from ..shared import ensure_dir, read_json, sha256_text, utc_now, write_json
8
8
 
9
+ NODE_TRACE_SCHEMA_VERSION = 2
10
+
9
11
 
10
12
  def _format_state_label(value: str | None) -> str:
11
13
  normalized = str(value or "").strip().replace("_", " ").replace("-", " ")
@@ -36,6 +38,23 @@ def _normalize_branch_name(value: object, *, fallback: str) -> str:
36
38
  return text or fallback
37
39
 
38
40
 
41
+ def _infer_stage_from_branch_name(value: object) -> str | None:
42
+ normalized = str(value or "").strip().lower()
43
+ if not normalized:
44
+ return None
45
+ if normalized.startswith("analysis/"):
46
+ return "analysis"
47
+ if normalized.startswith("run/"):
48
+ return "experiment"
49
+ if normalized.startswith("idea/"):
50
+ return "idea"
51
+ if normalized.startswith("paper/") or normalized.startswith("write/"):
52
+ return "writing"
53
+ if normalized.startswith("baseline/"):
54
+ return "baseline"
55
+ return None
56
+
57
+
39
58
  def _infer_stage_from_skill(skill_id: object) -> str | None:
40
59
  normalized = str(skill_id or "").strip().lower()
41
60
  if not normalized:
@@ -240,6 +259,7 @@ def _resolve_entry_context(
240
259
  or _infer_stage_from_artifact((((run_artifact or {}).get("record") or {}) if run_artifact else {}))
241
260
  or (run_context or {}).get("stage_key")
242
261
  or _infer_stage_from_event_type(raw_event_type)
262
+ or _infer_stage_from_branch_name(branch_name)
243
263
  or "general"
244
264
  )
245
265
  return {
@@ -440,6 +460,7 @@ class QuestNodeTraceManager:
440
460
  source_signature = sha256_text(
441
461
  json.dumps(
442
462
  {
463
+ "schema_version": NODE_TRACE_SCHEMA_VERSION,
443
464
  "entries": workflow.get("entries") or [],
444
465
  "branch": (snapshot or {}).get("branch"),
445
466
  },
@@ -451,6 +472,7 @@ class QuestNodeTraceManager:
451
472
  if (
452
473
  isinstance(existing, dict)
453
474
  and existing.get("quest_id") == quest_id
475
+ and existing.get("schema_version") == NODE_TRACE_SCHEMA_VERSION
454
476
  and existing.get("source_signature") == source_signature
455
477
  and isinstance(existing.get("items"), list)
456
478
  ):
@@ -545,6 +567,7 @@ class QuestNodeTraceManager:
545
567
 
546
568
  payload = {
547
569
  "quest_id": quest_id,
570
+ "schema_version": NODE_TRACE_SCHEMA_VERSION,
548
571
  "generated_at": utc_now(),
549
572
  "source_signature": source_signature,
550
573
  "items": items,
@@ -19,7 +19,7 @@ except ImportError: # pragma: no cover
19
19
 
20
20
  from ..artifact.metrics import build_metrics_timeline, extract_latest_metric
21
21
  from ..config import ConfigManager
22
- from ..connector_runtime import conversation_identity_key, normalize_conversation_id
22
+ from ..connector_runtime import conversation_identity_key, normalize_conversation_id, parse_conversation_id
23
23
  from ..gitops import current_branch, export_git_graph, head_commit, init_repo
24
24
  from ..home import repo_root
25
25
  from ..registries import BaselineRegistry
@@ -64,6 +64,35 @@ class QuestService:
64
64
  def _quest_root(self, quest_id: str) -> Path:
65
65
  return self.quests_root / quest_id
66
66
 
67
+ def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
68
+ local_present = False
69
+ external_source: str | None = None
70
+ for raw in sources or []:
71
+ normalized = self._normalize_binding_source(raw)
72
+ if not normalized:
73
+ continue
74
+ if normalized == "local:default":
75
+ local_present = True
76
+ continue
77
+ parsed = parse_conversation_id(normalized)
78
+ connector = str((parsed or {}).get("connector") or "").strip().lower()
79
+ if connector == "local":
80
+ local_present = True
81
+ continue
82
+ external_source = normalized
83
+ if external_source:
84
+ return ["local:default", external_source]
85
+ if local_present:
86
+ return ["local:default"]
87
+ return ["local:default"]
88
+
89
+ def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
90
+ bindings_path = quest_root / ".ds" / "bindings.json"
91
+ payload = read_json(bindings_path, {"sources": ["local:default"]})
92
+ raw_sources = payload.get("sources") if isinstance(payload, dict) else ["local:default"]
93
+ sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else ["local:default"])
94
+ return {"sources": sources}
95
+
67
96
  def preferred_locale(self, quest_root: Path | None = None) -> str:
68
97
  if quest_root is not None:
69
98
  try:
@@ -738,7 +767,7 @@ class QuestService:
738
767
  "last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
739
768
  "last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
740
769
  "last_delivered_at": runtime_state.get("last_delivered_at"),
741
- "bound_conversations": (self._read_cached_json(quest_root / ".ds" / "bindings.json", {}).get("sources") or ["local:default"]),
770
+ "bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
742
771
  "created_at": quest_yaml.get("created_at"),
743
772
  "updated_at": quest_yaml.get("updated_at"),
744
773
  "branch": research_state.get("current_workspace_branch") or research_state.get("research_head_branch"),
@@ -1093,7 +1122,7 @@ class QuestService:
1093
1122
  "last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
1094
1123
  "last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
1095
1124
  "last_delivered_at": runtime_state.get("last_delivered_at"),
1096
- "bound_conversations": (self._read_cached_json(quest_root / ".ds" / "bindings.json", {}).get("sources") or ["local:default"]),
1125
+ "bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
1097
1126
  "created_at": quest_yaml.get("created_at"),
1098
1127
  "updated_at": quest_yaml.get("updated_at"),
1099
1128
  "branch": current_branch(workspace_root),
@@ -1362,61 +1391,30 @@ class QuestService:
1362
1391
  def bind_source(self, quest_id: str, source: str) -> dict:
1363
1392
  quest_root = self._quest_root(quest_id)
1364
1393
  bindings_path = quest_root / ".ds" / "bindings.json"
1365
- bindings = read_json(bindings_path, {"sources": []})
1394
+ bindings = self._binding_sources_payload(quest_root)
1366
1395
  normalized_source = self._normalize_binding_source(source)
1367
- normalized_key = conversation_identity_key(normalized_source)
1368
- changed = False
1369
- replaced = False
1370
- sources: list[str] = []
1371
- for item in list(bindings.get("sources") or []):
1372
- existing = self._normalize_binding_source(str(item))
1373
- if conversation_identity_key(existing) == normalized_key:
1374
- if not replaced:
1375
- sources.append(normalized_source)
1376
- replaced = True
1377
- if existing != normalized_source:
1378
- changed = True
1379
- else:
1380
- changed = True
1381
- continue
1382
- sources.append(existing)
1383
- if existing != item:
1384
- changed = True
1385
- if not replaced:
1386
- sources.append(normalized_source)
1387
- changed = True
1396
+ next_sources = self._normalized_binding_sources([*(bindings.get("sources") or []), normalized_source])
1397
+ changed = list(bindings.get("sources") or []) != next_sources
1388
1398
  if changed:
1389
- bindings["sources"] = sources
1399
+ bindings["sources"] = next_sources
1390
1400
  write_json(bindings_path, bindings)
1391
1401
  return bindings
1392
1402
 
1393
1403
  def binding_sources(self, quest_id: str) -> list[str]:
1394
1404
  quest_root = self._quest_root(quest_id)
1395
- bindings_path = quest_root / ".ds" / "bindings.json"
1396
- bindings = read_json(bindings_path, {"sources": ["local:default"]})
1397
- sources = [self._normalize_binding_source(item) for item in (bindings.get("sources") or [])]
1398
- return [item for item in sources if item]
1405
+ return list(self._binding_sources_payload(quest_root).get("sources") or ["local:default"])
1399
1406
 
1400
1407
  def set_binding_sources(self, quest_id: str, sources: list[str]) -> dict:
1401
1408
  quest_root = self._quest_root(quest_id)
1402
1409
  bindings_path = quest_root / ".ds" / "bindings.json"
1403
- normalized_sources = [self._normalize_binding_source(item) for item in sources]
1404
- ordered: list[str] = []
1405
- seen: set[str] = set()
1406
- for item in normalized_sources:
1407
- key = conversation_identity_key(item)
1408
- if not item or key in seen:
1409
- continue
1410
- seen.add(key)
1411
- ordered.append(item)
1412
- payload = {"sources": ordered}
1410
+ payload = {"sources": self._normalized_binding_sources(sources)}
1413
1411
  write_json(bindings_path, payload)
1414
1412
  return payload
1415
1413
 
1416
1414
  def unbind_source(self, quest_id: str, source: str) -> dict:
1417
1415
  quest_root = self._quest_root(quest_id)
1418
1416
  bindings_path = quest_root / ".ds" / "bindings.json"
1419
- bindings = read_json(bindings_path, {"sources": []})
1417
+ bindings = self._binding_sources_payload(quest_root)
1420
1418
  normalized_source = self._normalize_binding_source(source)
1421
1419
  normalized_key = conversation_identity_key(normalized_source)
1422
1420
  changed = False
@@ -1429,8 +1427,11 @@ class QuestService:
1429
1427
  sources.append(existing)
1430
1428
  if existing != item:
1431
1429
  changed = True
1430
+ normalized_sources = self._normalized_binding_sources(sources)
1431
+ if normalized_sources != list(bindings.get("sources") or []):
1432
+ changed = True
1432
1433
  if changed:
1433
- bindings["sources"] = sources
1434
+ bindings["sources"] = normalized_sources
1434
1435
  write_json(bindings_path, bindings)
1435
1436
  return bindings
1436
1437
 
@@ -1824,6 +1825,12 @@ class QuestService:
1824
1825
  resolved_selection = dict(selection or {})
1825
1826
  selection_ref = str(resolved_selection.get("selection_ref") or "").strip()
1826
1827
  selection_type = str(resolved_selection.get("selection_type") or "stage_node").strip() or None
1828
+ if (
1829
+ selection_type == "branch_node"
1830
+ and selection_ref
1831
+ and not str(resolved_selection.get("branch_name") or "").strip()
1832
+ ):
1833
+ resolved_selection["branch_name"] = selection_ref
1827
1834
  trace = None
1828
1835
  if selection_ref:
1829
1836
  try:
@@ -2080,7 +2087,18 @@ class QuestService:
2080
2087
  if document_id.startswith(("questpath::", "memory::"))
2081
2088
  else workspace_root
2082
2089
  )
2083
- path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
2090
+ try:
2091
+ path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
2092
+ except FileNotFoundError:
2093
+ legacy_relative = None
2094
+ if document_id.startswith("path::"):
2095
+ legacy_relative = document_id.split("::", 1)[1].lstrip("/")
2096
+ if legacy_relative and legacy_relative.startswith("literature/arxiv/"):
2097
+ path, writable, scope, source_kind = self._resolve_document(
2098
+ quest_root, f"questpath::{legacy_relative}"
2099
+ )
2100
+ else:
2101
+ raise
2084
2102
  renderer_hint, mime_type = self._renderer_hint_for(path)
2085
2103
  is_text = self._is_text_document(path, mime_type, renderer_hint)
2086
2104
  content = read_text(path) if is_text else ""
@@ -3549,7 +3567,35 @@ def _tool_name(event: dict, item: dict) -> str:
3549
3567
  return "tool"
3550
3568
 
3551
3569
 
3570
+ def _structured_text(value: object) -> str:
3571
+ if value is None:
3572
+ return ""
3573
+ if isinstance(value, str):
3574
+ return value.strip()
3575
+ try:
3576
+ return json.dumps(value, ensure_ascii=False, indent=2)
3577
+ except TypeError:
3578
+ return str(value)
3579
+
3580
+
3581
+ def _is_bash_exec_item(event: dict, item: dict) -> bool:
3582
+ server = str(item.get("server") or event.get("server") or "").strip()
3583
+ tool = str(item.get("tool") or event.get("tool") or "").strip()
3584
+ return server == "bash_exec" and tool == "bash_exec"
3585
+
3586
+
3552
3587
  def _tool_args(event: dict, item: dict) -> str:
3588
+ if _is_bash_exec_item(event, item):
3589
+ for value in (
3590
+ item.get("arguments"),
3591
+ event.get("arguments"),
3592
+ item.get("input"),
3593
+ event.get("input"),
3594
+ ):
3595
+ text = _structured_text(value)
3596
+ if text:
3597
+ return text
3598
+ return ""
3553
3599
  for value in (
3554
3600
  item.get("command"),
3555
3601
  item.get("query"),
@@ -3569,6 +3615,21 @@ def _tool_args(event: dict, item: dict) -> str:
3569
3615
 
3570
3616
 
3571
3617
  def _tool_output(event: dict, item: dict) -> str:
3618
+ if _is_bash_exec_item(event, item):
3619
+ for value in (
3620
+ item.get("result"),
3621
+ item.get("output"),
3622
+ item.get("content"),
3623
+ event.get("result"),
3624
+ event.get("output"),
3625
+ event.get("content"),
3626
+ item.get("aggregated_output"),
3627
+ event.get("aggregated_output"),
3628
+ ):
3629
+ text = _structured_text(value)
3630
+ if text:
3631
+ return text
3632
+ return ""
3572
3633
  for value in (
3573
3634
  item.get("aggregated_output"),
3574
3635
  item.get("changes"),
@@ -3611,17 +3672,25 @@ def _mcp_tool_metadata(*, quest_id: str, run_id: str, server: str, tool: str, it
3611
3672
  for key in ("command", "workdir", "mode", "timeout_seconds", "comment"):
3612
3673
  if key in arguments:
3613
3674
  metadata[key] = arguments.get(key)
3675
+ if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
3676
+ metadata["bash_id"] = arguments.get("id")
3614
3677
  result_payload = _mcp_result_payload(item)
3615
3678
  if server == "bash_exec" and tool == "bash_exec":
3616
3679
  for key in (
3617
3680
  "bash_id",
3618
3681
  "status",
3682
+ "command",
3683
+ "workdir",
3684
+ "cwd",
3685
+ "kind",
3686
+ "comment",
3619
3687
  "started_at",
3620
3688
  "finished_at",
3621
3689
  "exit_code",
3622
3690
  "stop_reason",
3623
3691
  "last_progress",
3624
3692
  "log_path",
3693
+ "watchdog_after_seconds",
3625
3694
  ):
3626
3695
  if key in result_payload:
3627
3696
  metadata[key] = result_payload.get(key)
@@ -153,8 +153,68 @@ class QuestStageViewBuilder:
153
153
  return candidate
154
154
  return self.quest_root
155
155
 
156
+ def _infer_stage_from_branch_name(self) -> str | None:
157
+ normalized = str(self.branch_name or "").strip().lower()
158
+ if not normalized:
159
+ return None
160
+ if normalized.startswith("analysis/"):
161
+ return "analysis"
162
+ if normalized.startswith("run/"):
163
+ return "experiment"
164
+ if normalized.startswith("idea/"):
165
+ return "idea"
166
+ if normalized.startswith("paper/") or normalized.startswith("write/"):
167
+ return "paper"
168
+ if normalized.startswith("baseline/"):
169
+ return "baseline"
170
+ return None
171
+
172
+ def _has_paper_state(self) -> bool:
173
+ paper_root = self._paper_root()
174
+ return bool(
175
+ self._paper_candidates()
176
+ or (paper_root / "selected_outline.json").exists()
177
+ or (paper_root / "draft.md").exists()
178
+ or self._paper_bundle_manifest()
179
+ )
180
+
181
+ def _resolve_effective_stage_key(self) -> str:
182
+ normalized = normalize_stage_key(self.stage_key)
183
+ if normalized in {"baseline", "idea", "experiment", "analysis", "paper"}:
184
+ return normalized
185
+ if normalized != "general":
186
+ return normalized
187
+
188
+ inferred = self._infer_stage_from_branch_name()
189
+ if inferred:
190
+ return inferred
191
+ if self._analysis_stage_items(None):
192
+ return "analysis"
193
+ if self._experiment_stage_items():
194
+ return "experiment"
195
+ if self._idea_stage_items():
196
+ return "idea"
197
+ if self._has_paper_state():
198
+ return "paper"
199
+ if self._baseline_stage_items():
200
+ return "baseline"
201
+ return normalized
202
+
203
+ @staticmethod
204
+ def _artifact_detail(item: dict[str, Any] | None, payload: dict[str, Any]) -> dict[str, Any] | None:
205
+ if not isinstance(payload, dict) or not payload:
206
+ return None
207
+ record = dict(item or {})
208
+ return {
209
+ "artifact_id": payload.get("artifact_id") or payload.get("id"),
210
+ "artifact_kind": payload.get("kind"),
211
+ "artifact_path": record.get("path"),
212
+ "payload": payload,
213
+ }
214
+
156
215
  def build(self) -> dict[str, Any]:
157
216
  selection_type = str(self.selection.get("selection_type") or "").strip()
217
+ self.stage_key = self._resolve_effective_stage_key()
158
218
  if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
159
219
  return self._build_branch()
160
220
  if self.stage_key == "baseline":
@@ -855,7 +915,8 @@ class QuestStageViewBuilder:
855
915
  "draft_markdown": draft_markdown,
856
916
  "literature_files": literature_files,
857
917
  "decision_reason": payload.get("reason"),
858
- }
918
+ },
919
+ "latest_artifact": self._artifact_detail(latest, payload),
859
920
  },
860
921
  lineage_intent=lineage_intent,
861
922
  idea_draft_path=draft_md_rel_path,
@@ -1084,7 +1145,8 @@ class QuestStageViewBuilder:
1084
1145
  else None,
1085
1146
  "analysis_summary_path": self._relative_path_or_raw(analysis_summary_path),
1086
1147
  "analysis_summary_markdown": analysis_summary_markdown,
1087
- }
1148
+ },
1149
+ "latest_artifact": self._artifact_detail(latest_experiment_item or latest_idea_item, latest_experiment_payload or latest_idea_payload),
1088
1150
  },
1089
1151
  lineage_intent=lineage_intent,
1090
1152
  idea_draft_path=idea_draft_rel_path,
@@ -1198,7 +1260,8 @@ class QuestStageViewBuilder:
1198
1260
  "trace_summary": trace_summary,
1199
1261
  "trace_markdown": trace_markdown,
1200
1262
  "trace_actions": self._recent_trace_actions(),
1201
- }
1263
+ },
1264
+ "latest_artifact": self._artifact_detail(latest, payload),
1202
1265
  },
1203
1266
  )
1204
1267
 
@@ -1388,10 +1451,12 @@ class QuestStageViewBuilder:
1388
1451
  "todo_manifest_markdown": self._markdown_body_for_path(manifest.get("todo_manifest_path")),
1389
1452
  "summary_path": self._relative_path_or_raw(summary_path) if summary_path else None,
1390
1453
  "summary_markdown": summary_markdown,
1454
+ "manifest_payload": manifest,
1391
1455
  "trace_summary": trace_summary,
1392
1456
  "trace_markdown": self._trace_markdown(),
1393
1457
  "trace_actions": self._recent_trace_actions(),
1394
- }
1458
+ },
1459
+ "latest_artifact": self._artifact_detail(latest, latest_payload),
1395
1460
  },
1396
1461
  )
1397
1462
 
@@ -1532,6 +1597,7 @@ class QuestStageViewBuilder:
1532
1597
  "latex_root_path": latex_root_rel,
1533
1598
  "main_tex_path": main_tex_rel,
1534
1599
  },
1535
- }
1600
+ },
1601
+ "latest_artifact": self._artifact_detail(paper_items[-1] if paper_items else None, self._payload(paper_items[-1] if paper_items else {})),
1536
1602
  },
1537
1603
  )
@@ -35,6 +35,17 @@ def _compact_text(value: object, *, limit: int = 1200) -> str:
35
35
  return text[: limit - 1].rstrip() + "…"
36
36
 
37
37
 
38
+ def _structured_text(value: object) -> str:
39
+ if value is None:
40
+ return ""
41
+ if isinstance(value, str):
42
+ return value.strip()
43
+ try:
44
+ return json.dumps(value, ensure_ascii=False, indent=2)
45
+ except TypeError:
46
+ return str(value)
47
+
48
+
38
49
  def _iter_event_texts(event: dict[str, Any]) -> list[str]:
39
50
  texts: list[str] = []
40
51
  for key in ("text", "content", "message"):
@@ -184,7 +195,24 @@ def _tool_name(event: dict[str, Any], item: dict[str, Any]) -> str:
184
195
  return "tool"
185
196
 
186
197
 
198
+ def _is_bash_exec_item(event: dict[str, Any], item: dict[str, Any]) -> bool:
199
+ server = str(item.get("server") or event.get("server") or "").strip()
200
+ tool = str(item.get("tool") or event.get("tool") or "").strip()
201
+ return server == "bash_exec" and tool == "bash_exec"
202
+
203
+
187
204
  def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
205
+ if _is_bash_exec_item(event, item):
206
+ for value in (
207
+ item.get("arguments"),
208
+ event.get("arguments"),
209
+ item.get("input"),
210
+ event.get("input"),
211
+ ):
212
+ text = _structured_text(value)
213
+ if text:
214
+ return text
215
+ return ""
188
216
  for value in (
189
217
  item.get("command"),
190
218
  item.get("query"),
@@ -204,6 +232,21 @@ def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
204
232
 
205
233
 
206
234
  def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
235
+ if _is_bash_exec_item(event, item):
236
+ for value in (
237
+ item.get("result"),
238
+ item.get("output"),
239
+ item.get("content"),
240
+ event.get("result"),
241
+ event.get("output"),
242
+ event.get("content"),
243
+ item.get("aggregated_output"),
244
+ event.get("aggregated_output"),
245
+ ):
246
+ text = _structured_text(value)
247
+ if text:
248
+ return text
249
+ return ""
207
250
  for value in (
208
251
  item.get("aggregated_output"),
209
252
  item.get("changes"),
@@ -253,10 +296,12 @@ def _mcp_tool_metadata(
253
296
  metadata["workdir"] = arguments.get("workdir")
254
297
  if isinstance(arguments.get("mode"), str):
255
298
  metadata["mode"] = arguments.get("mode")
256
- if isinstance(arguments.get("timeout_seconds"), int):
299
+ if arguments.get("timeout_seconds") is not None:
257
300
  metadata["timeout_seconds"] = arguments.get("timeout_seconds")
258
301
  if "comment" in arguments:
259
302
  metadata["comment"] = arguments.get("comment")
303
+ if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
304
+ metadata["bash_id"] = arguments.get("id")
260
305
  metadata["session_id"] = f"quest:{quest_id}"
261
306
  metadata["agent_id"] = "pi"
262
307
  metadata["agent_instance_id"] = run_id
@@ -266,12 +311,18 @@ def _mcp_tool_metadata(
266
311
  for key in (
267
312
  "bash_id",
268
313
  "status",
314
+ "command",
315
+ "workdir",
316
+ "cwd",
317
+ "kind",
318
+ "comment",
269
319
  "started_at",
270
320
  "finished_at",
271
321
  "exit_code",
272
322
  "stop_reason",
273
323
  "last_progress",
274
324
  "log_path",
325
+ "watchdog_after_seconds",
275
326
  ):
276
327
  if key in result_payload:
277
328
  metadata[key] = result_payload.get(key)
@@ -758,6 +809,7 @@ class CodexRunner:
758
809
  workspace_root = request.worktree_root or request.quest_root
759
810
  resolved_binary = resolve_runner_binary(self.binary, runner_name="codex")
760
811
  resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
812
+ normalized_model = str(request.model or "").strip()
761
813
  command = [
762
814
  resolved_binary or self.binary,
763
815
  "--search",
@@ -766,9 +818,9 @@ class CodexRunner:
766
818
  "--cd",
767
819
  str(workspace_root),
768
820
  "--skip-git-repo-check",
769
- "--model",
770
- request.model,
771
821
  ]
822
+ if normalized_model.lower() not in {"", "inherit", "default", "codex-default"}:
823
+ command.extend(["--model", normalized_model])
772
824
  if request.approval_policy:
773
825
  command.extend(["-c", f'approval_policy="{request.approval_policy}"'])
774
826
  reasoning_effort = request.reasoning_effort
@@ -0,0 +1 @@
1
+ from .connector.weixin_support import * # noqa: F401,F403
@@ -11,5 +11,7 @@
11
11
  - lingzhu_safety_rule: request only actions that are clearly justified by the current quest and understandable to the human user
12
12
  - lingzhu_text_rule: even when requesting `surface_actions`, always include a clear text explanation of what is happening and why
13
13
  - lingzhu_reply_style_rule: for Lingzhu-facing user-visible text sent through `artifact.interact(...)`, keep the message clear, concise, respectful, and high-information-density
14
- - lingzhu_reply_length_rule: for each Lingzhu-facing `artifact.interact(...)` message, normally answer in at most 2 to 3 sentences unless the user explicitly asks for more detail
14
+ - lingzhu_reply_length_rule: for each Lingzhu-facing `artifact.interact(...)` message, normally keep the text within about 20 Chinese characters or one very short sentence unless the user explicitly asks for more detail
15
15
  - lingzhu_summary_first_rule: in Lingzhu-facing `artifact.interact(...)` messages, usually give only the synopsis and key facts needed for the user's next decision or understanding; avoid long preambles, repetition, and low-signal detail
16
+ - lingzhu_task_gate_rule: only treat a Lingzhu user utterance as a new quest instruction when the text explicitly starts with `我现在的任务是`; otherwise assume the device is polling for queued progress or buffered replies
17
+ - lingzhu_poll_rule: when Lingzhu is polling rather than giving a new task, return only the buffered progress checkpoints or the latest short status; do not reinterpret the poll text as a fresh instruction