@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
@@ -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
  )
@@ -19,6 +19,11 @@ from ..shared import append_jsonl, ensure_dir, generate_id, read_yaml, resolve_r
19
19
  from ..web_search import extract_web_search_payload
20
20
  from .base import RunRequest, RunResult
21
21
 
22
+ _TOOL_EVENT_ARGS_TEXT_LIMIT = 8_000
23
+ _TOOL_EVENT_OUTPUT_TEXT_LIMIT = 16_000
24
+ _MAX_QUEST_EVENT_JSON_BYTES = 2_000_000
25
+ _OVERSIZED_EVENT_PREVIEW_TEXT_LIMIT = 12_000
26
+
22
27
 
23
28
  def _compact_text(value: object, *, limit: int = 1200) -> str:
24
29
  if value is None:
@@ -35,6 +40,96 @@ def _compact_text(value: object, *, limit: int = 1200) -> str:
35
40
  return text[: limit - 1].rstrip() + "…"
36
41
 
37
42
 
43
+ def _truncate_leaf_text(text: str, *, limit: int) -> str:
44
+ if limit <= 0 or len(text) <= limit:
45
+ return text
46
+ head = max(int(limit * 0.7), 256)
47
+ tail = max(limit - head - 64, 128)
48
+ omitted = max(len(text) - head - tail, 0)
49
+ return f"{text[:head].rstrip()}\n...[truncated {omitted} chars]...\n{text[-tail:].lstrip()}"
50
+
51
+
52
+ def _truncate_structured_value(value: object, *, string_limit: int) -> object:
53
+ if isinstance(value, str):
54
+ return _truncate_leaf_text(value.strip(), limit=string_limit)
55
+ if isinstance(value, list):
56
+ return [_truncate_structured_value(item, string_limit=string_limit) for item in value[:200]]
57
+ if isinstance(value, dict):
58
+ truncated: dict[object, object] = {}
59
+ for index, (key, item) in enumerate(value.items()):
60
+ if index >= 200:
61
+ truncated["__truncated__"] = f"truncated remaining {len(value) - 200} item(s)"
62
+ break
63
+ truncated[key] = _truncate_structured_value(item, string_limit=string_limit)
64
+ return truncated
65
+ return value
66
+
67
+
68
+ def _structured_text(value: object, *, limit: int | None = None) -> str:
69
+ if value is None:
70
+ return ""
71
+ if isinstance(value, str):
72
+ return _truncate_leaf_text(value.strip(), limit=limit or len(value))
73
+ normalized_value = _truncate_structured_value(value, string_limit=max(limit or _TOOL_EVENT_OUTPUT_TEXT_LIMIT, 512))
74
+ try:
75
+ return json.dumps(normalized_value, ensure_ascii=False, indent=2)
76
+ except TypeError:
77
+ return _truncate_leaf_text(str(value), limit=limit or _TOOL_EVENT_OUTPUT_TEXT_LIMIT)
78
+
79
+
80
+ def _encoded_json_size(value: object) -> int:
81
+ try:
82
+ return len(json.dumps(value, ensure_ascii=False).encode("utf-8"))
83
+ except Exception:
84
+ return len(str(value).encode("utf-8", errors="ignore"))
85
+
86
+
87
+ def _compact_tool_event_payload(payload: dict[str, Any]) -> dict[str, Any]:
88
+ if _encoded_json_size(payload) <= _MAX_QUEST_EVENT_JSON_BYTES:
89
+ return payload
90
+
91
+ compacted = dict(payload)
92
+ output_text = str(compacted.get("output") or "")
93
+ if output_text:
94
+ compacted["output_bytes"] = len(output_text.encode("utf-8", errors="ignore"))
95
+ compacted["output"] = _truncate_leaf_text(
96
+ output_text,
97
+ limit=_OVERSIZED_EVENT_PREVIEW_TEXT_LIMIT,
98
+ )
99
+ compacted["output_truncated"] = True
100
+ args_text = str(compacted.get("args") or "")
101
+ if args_text and _encoded_json_size(compacted) > _MAX_QUEST_EVENT_JSON_BYTES:
102
+ compacted["args"] = _truncate_leaf_text(args_text, limit=4_000)
103
+ compacted["args_truncated"] = True
104
+ if _encoded_json_size(compacted) > _MAX_QUEST_EVENT_JSON_BYTES:
105
+ metadata = compacted.get("metadata")
106
+ if isinstance(metadata, dict):
107
+ allowed_keys = {
108
+ "mcp_server",
109
+ "mcp_tool",
110
+ "bash_id",
111
+ "status",
112
+ "command",
113
+ "workdir",
114
+ "cwd",
115
+ "started_at",
116
+ "finished_at",
117
+ "exit_code",
118
+ "stop_reason",
119
+ "log_path",
120
+ }
121
+ compacted["metadata"] = {
122
+ key: metadata.get(key)
123
+ for key in allowed_keys
124
+ if key in metadata
125
+ }
126
+ compacted["metadata_truncated"] = True
127
+ if _encoded_json_size(compacted) > _MAX_QUEST_EVENT_JSON_BYTES:
128
+ compacted["output"] = _compact_text(compacted.get("output"), limit=2_000)
129
+ compacted["output_truncated"] = True
130
+ return compacted
131
+
132
+
38
133
  def _iter_event_texts(event: dict[str, Any]) -> list[str]:
39
134
  texts: list[str] = []
40
135
  for key in ("text", "content", "message"):
@@ -184,7 +279,24 @@ def _tool_name(event: dict[str, Any], item: dict[str, Any]) -> str:
184
279
  return "tool"
185
280
 
186
281
 
282
+ def _is_bash_exec_item(event: dict[str, Any], item: dict[str, Any]) -> bool:
283
+ server = str(item.get("server") or event.get("server") or "").strip()
284
+ tool = str(item.get("tool") or event.get("tool") or "").strip()
285
+ return server == "bash_exec" and tool == "bash_exec"
286
+
287
+
187
288
  def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
289
+ if _is_bash_exec_item(event, item):
290
+ for value in (
291
+ item.get("arguments"),
292
+ event.get("arguments"),
293
+ item.get("input"),
294
+ event.get("input"),
295
+ ):
296
+ text = _structured_text(value, limit=_TOOL_EVENT_ARGS_TEXT_LIMIT)
297
+ if text:
298
+ return text
299
+ return ""
188
300
  for value in (
189
301
  item.get("command"),
190
302
  item.get("query"),
@@ -204,6 +316,21 @@ def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
204
316
 
205
317
 
206
318
  def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
319
+ if _is_bash_exec_item(event, item):
320
+ for value in (
321
+ item.get("result"),
322
+ item.get("output"),
323
+ item.get("content"),
324
+ event.get("result"),
325
+ event.get("output"),
326
+ event.get("content"),
327
+ item.get("aggregated_output"),
328
+ event.get("aggregated_output"),
329
+ ):
330
+ text = _structured_text(value, limit=_TOOL_EVENT_OUTPUT_TEXT_LIMIT)
331
+ if text:
332
+ return text
333
+ return ""
207
334
  for value in (
208
335
  item.get("aggregated_output"),
209
336
  item.get("changes"),
@@ -253,10 +380,12 @@ def _mcp_tool_metadata(
253
380
  metadata["workdir"] = arguments.get("workdir")
254
381
  if isinstance(arguments.get("mode"), str):
255
382
  metadata["mode"] = arguments.get("mode")
256
- if isinstance(arguments.get("timeout_seconds"), int):
383
+ if arguments.get("timeout_seconds") is not None:
257
384
  metadata["timeout_seconds"] = arguments.get("timeout_seconds")
258
385
  if "comment" in arguments:
259
386
  metadata["comment"] = arguments.get("comment")
387
+ if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
388
+ metadata["bash_id"] = arguments.get("id")
260
389
  metadata["session_id"] = f"quest:{quest_id}"
261
390
  metadata["agent_id"] = "pi"
262
391
  metadata["agent_instance_id"] = run_id
@@ -266,12 +395,18 @@ def _mcp_tool_metadata(
266
395
  for key in (
267
396
  "bash_id",
268
397
  "status",
398
+ "command",
399
+ "workdir",
400
+ "cwd",
401
+ "kind",
402
+ "comment",
269
403
  "started_at",
270
404
  "finished_at",
271
405
  "exit_code",
272
406
  "stop_reason",
273
407
  "last_progress",
274
408
  "log_path",
409
+ "watchdog_after_seconds",
275
410
  ):
276
411
  if key in result_payload:
277
412
  metadata[key] = result_payload.get(key)
@@ -310,7 +445,7 @@ def _tool_event(
310
445
  "raw_event_type": event_type,
311
446
  "created_at": created_at,
312
447
  }
313
- return {
448
+ return _compact_tool_event_payload({
314
449
  "event_id": generate_id("evt"),
315
450
  "type": "runner.tool_result",
316
451
  "quest_id": quest_id,
@@ -324,7 +459,7 @@ def _tool_event(
324
459
  "output": _tool_output(event, item),
325
460
  "raw_event_type": event_type,
326
461
  "created_at": created_at,
327
- }
462
+ })
328
463
 
329
464
  if item_type == "web_search":
330
465
  tool_call_id = _tool_call_id(event, item)
@@ -348,7 +483,7 @@ def _tool_event(
348
483
  "raw_event_type": event_type,
349
484
  "created_at": created_at,
350
485
  }
351
- return {
486
+ return _compact_tool_event_payload({
352
487
  "event_id": generate_id("evt"),
353
488
  "type": "runner.tool_result",
354
489
  "quest_id": quest_id,
@@ -363,13 +498,13 @@ def _tool_event(
363
498
  "metadata": metadata,
364
499
  "raw_event_type": event_type,
365
500
  "created_at": created_at,
366
- }
501
+ })
367
502
 
368
503
  if item_type == "file_change":
369
504
  tool_call_id = _tool_call_id(event, item)
370
505
  tool_name = "file_change"
371
506
  known_tool_names[tool_call_id] = tool_name
372
- return {
507
+ return _compact_tool_event_payload({
373
508
  "event_id": generate_id("evt"),
374
509
  "type": "runner.tool_result",
375
510
  "quest_id": quest_id,
@@ -382,7 +517,7 @@ def _tool_event(
382
517
  "output": _tool_output(event, item),
383
518
  "raw_event_type": event_type,
384
519
  "created_at": created_at,
385
- }
520
+ })
386
521
 
387
522
  if item_type == "mcp_tool_call":
388
523
  tool_call_id = _tool_call_id(event, item)
@@ -415,7 +550,7 @@ def _tool_event(
415
550
  "raw_event_type": event_type,
416
551
  "created_at": created_at,
417
552
  }
418
- return {
553
+ return _compact_tool_event_payload({
419
554
  "event_id": generate_id("evt"),
420
555
  "type": "runner.tool_result",
421
556
  "quest_id": quest_id,
@@ -432,7 +567,7 @@ def _tool_event(
432
567
  "metadata": metadata,
433
568
  "raw_event_type": event_type,
434
569
  "created_at": created_at,
435
- }
570
+ })
436
571
 
437
572
  if item_type in {"function_call", "custom_tool_call", "tool_call"} or "function_call" in event_type or "tool_call" in event_type:
438
573
  tool_call_id = _tool_call_id(event, item)
@@ -456,7 +591,7 @@ def _tool_event(
456
591
  if item_type in {"function_call_output", "custom_tool_call_output", "tool_result", "tool_call_output"} or "function_call_output" in event_type or "tool_result" in event_type:
457
592
  tool_call_id = _tool_call_id(event, item)
458
593
  tool_name = known_tool_names.get(tool_call_id) or _tool_name(event, item)
459
- return {
594
+ return _compact_tool_event_payload({
460
595
  "event_id": generate_id("evt"),
461
596
  "type": "runner.tool_result",
462
597
  "quest_id": quest_id,
@@ -470,7 +605,7 @@ def _tool_event(
470
605
  "output": _tool_output(event, item),
471
606
  "raw_event_type": event_type,
472
607
  "created_at": created_at,
473
- }
608
+ })
474
609
 
475
610
  return None
476
611
 
@@ -531,6 +666,12 @@ class CodexRunner:
531
666
  )
532
667
 
533
668
  env = dict(**os.environ)
669
+ runner_env = runner_config.get("env") if isinstance(runner_config.get("env"), dict) else {}
670
+ for key, value in runner_env.items():
671
+ env_key = str(key or "").strip()
672
+ if not env_key or value is None:
673
+ continue
674
+ env[env_key] = str(value)
534
675
  env["CODEX_HOME"] = str(codex_home)
535
676
  env["DEEPSCIENTIST_HOME"] = str(self.home)
536
677
  env["DS_HOME"] = str(self.home)
@@ -758,17 +899,25 @@ class CodexRunner:
758
899
  workspace_root = request.worktree_root or request.quest_root
759
900
  resolved_binary = resolve_runner_binary(self.binary, runner_name="codex")
760
901
  resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
902
+ profile = str(resolved_runner_config.get("profile") or "").strip()
903
+ normalized_model = str(request.model or "").strip()
761
904
  command = [
762
905
  resolved_binary or self.binary,
763
906
  "--search",
764
- "exec",
765
- "--json",
766
- "--cd",
767
- str(workspace_root),
768
- "--skip-git-repo-check",
769
- "--model",
770
- request.model,
771
907
  ]
908
+ if profile:
909
+ command.extend(["--profile", profile])
910
+ command.extend(
911
+ [
912
+ "exec",
913
+ "--json",
914
+ "--cd",
915
+ str(workspace_root),
916
+ "--skip-git-repo-check",
917
+ ]
918
+ )
919
+ if normalized_model.lower() not in {"", "inherit", "default", "codex-default"}:
920
+ command.extend(["--model", normalized_model])
772
921
  if request.approval_policy:
773
922
  command.extend(["-c", f'approval_policy="{request.approval_policy}"'])
774
923
  reasoning_effort = request.reasoning_effort
@@ -794,7 +943,9 @@ class CodexRunner:
794
943
  runner_config: dict[str, Any] | None = None,
795
944
  ) -> Path:
796
945
  target = ensure_dir(workspace_root / ".codex")
797
- source = Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))).expanduser()
946
+ resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
947
+ configured_home = str(resolved_runner_config.get("config_dir") or os.environ.get("CODEX_HOME") or str(Path.home() / ".codex"))
948
+ source = Path(configured_home).expanduser()
798
949
  for filename in ("config.toml", "auth.json"):
799
950
  source_path = source / filename
800
951
  target_path = target / filename
@@ -20,6 +20,8 @@ def _as_bool_env(name: str) -> bool:
20
20
  def codex_runtime_overrides() -> dict[str, str]:
21
21
  approval_policy = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_APPROVAL_POLICY"))
22
22
  sandbox_mode = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_SANDBOX_MODE"))
23
+ profile = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_PROFILE"))
24
+ model = _as_text(os.environ.get("DEEPSCIENTIST_CODEX_MODEL"))
23
25
 
24
26
  if _as_bool_env("DEEPSCIENTIST_CODEX_YOLO"):
25
27
  approval_policy = approval_policy or "never"
@@ -30,6 +32,10 @@ def codex_runtime_overrides() -> dict[str, str]:
30
32
  overrides["approval_policy"] = approval_policy
31
33
  if sandbox_mode:
32
34
  overrides["sandbox_mode"] = sandbox_mode
35
+ if profile:
36
+ overrides["profile"] = profile
37
+ if model:
38
+ overrides["model"] = model
33
39
  return overrides
34
40
 
35
41
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections import deque
3
4
  import hashlib
4
5
  import json
5
6
  import os
@@ -9,7 +10,7 @@ import subprocess
9
10
  import sys
10
11
  from datetime import UTC, datetime
11
12
  from pathlib import Path
12
- from typing import Any
13
+ from typing import Any, Iterator
13
14
  from uuid import uuid4
14
15
 
15
16
  try:
@@ -90,21 +91,39 @@ def append_jsonl(path: Path, payload: dict[str, Any]) -> None:
90
91
  handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
91
92
 
92
93
 
93
- def read_jsonl(path: Path) -> list[dict[str, Any]]:
94
+ def iter_jsonl(path: Path | str) -> Iterator[dict[str, Any]]:
95
+ path = Path(path)
94
96
  if not path.exists():
97
+ return
98
+ with path.open("r", encoding="utf-8") as handle:
99
+ for raw_line in handle:
100
+ line = raw_line.strip()
101
+ if not line:
102
+ continue
103
+ try:
104
+ payload = json.loads(line)
105
+ except json.JSONDecodeError:
106
+ continue
107
+ if isinstance(payload, dict):
108
+ yield payload
109
+
110
+
111
+ def read_jsonl(path: Path) -> list[dict[str, Any]]:
112
+ return list(iter_jsonl(path))
113
+
114
+
115
+ def count_jsonl(path: Path | str) -> int:
116
+ return sum(1 for _ in iter_jsonl(path))
117
+
118
+
119
+ def read_jsonl_tail(path: Path | str, limit: int) -> list[dict[str, Any]]:
120
+ normalized_limit = max(int(limit or 0), 0)
121
+ if normalized_limit <= 0:
95
122
  return []
96
- items: list[dict[str, Any]] = []
97
- for line in path.read_text(encoding="utf-8").splitlines():
98
- line = line.strip()
99
- if not line:
100
- continue
101
- try:
102
- payload = json.loads(line)
103
- except json.JSONDecodeError:
104
- continue
105
- if isinstance(payload, dict):
106
- items.append(payload)
107
- return items
123
+ items: deque[dict[str, Any]] = deque(maxlen=normalized_limit)
124
+ for payload in iter_jsonl(path):
125
+ items.append(payload)
126
+ return list(items)
108
127
 
109
128
 
110
129
  def read_yaml(path: Path, default: Any = None) -> Any:
@@ -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
@@ -10,7 +10,8 @@
10
10
  - qq_summary_first_rule: start with the conclusion the user cares about, then what it means, then the next action
11
11
  - qq_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete measure explicit whenever possible
12
12
  - qq_eta_rule: for baseline reproduction, main experiments, analysis experiments, and other important long-running research phases, include a rough ETA for the next meaningful result or the next update; if uncertain, say that and still give the next check-in window
13
- - qq_tool_call_keepalive_rule: for ordinary active work, prefer one concise QQ progress update after roughly 10 tool calls when there is already a human-meaningful delta, and do not let work drift beyond roughly 20 tool calls or about 15 minutes without a user-visible checkpoint
13
+ - qq_tool_call_keepalive_rule: for ordinary active work, prefer one concise QQ progress update after roughly 6 tool calls when there is already a human-meaningful delta, and do not let work drift beyond roughly 12 tool calls or about 8 minutes without a user-visible checkpoint
14
+ - qq_read_plan_keepalive_rule: if the active work is still mostly reading, comparison, or planning, do not wait too long for a "big result"; send a short QQ-facing checkpoint after about 5 consecutive tool calls if the user would otherwise see silence
14
15
  - qq_internal_detail_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless the user asked for them or the detail changes the recommended action
15
16
  - qq_translation_rule: convert internal execution and file-management work into user value, such as saying the baseline record is now organized for easier later comparison instead of listing touched files
16
17
  - qq_preflight_rule: before sending a QQ progress update, rewrite it if it still sounds like a monitoring log, execution diary, or file inventory