@researai/deepscientist 1.5.7 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -21
- package/README.md +8 -4
- package/bin/ds.js +224 -9
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/07_MEMORY_AND_MCP.md +40 -3
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
- package/install.sh +34 -0
- package/package.json +2 -2
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +1 -0
- package/src/deepscientist/artifact/metrics.py +814 -83
- package/src/deepscientist/artifact/schemas.py +1 -0
- package/src/deepscientist/artifact/service.py +2001 -229
- package/src/deepscientist/bash_exec/monitor.py +1 -1
- package/src/deepscientist/bash_exec/service.py +17 -9
- package/src/deepscientist/channels/qq.py +17 -0
- package/src/deepscientist/channels/relay.py +16 -0
- package/src/deepscientist/config/models.py +6 -0
- package/src/deepscientist/config/service.py +70 -2
- package/src/deepscientist/daemon/api/handlers.py +414 -14
- package/src/deepscientist/daemon/api/router.py +4 -0
- package/src/deepscientist/daemon/app.py +292 -21
- package/src/deepscientist/gitops/diff.py +6 -10
- package/src/deepscientist/mcp/server.py +191 -40
- package/src/deepscientist/prompts/builder.py +65 -19
- package/src/deepscientist/quest/node_traces.py +129 -2
- package/src/deepscientist/quest/service.py +140 -34
- package/src/deepscientist/quest/stage_views.py +175 -33
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +1 -1
- package/src/prompts/connectors/qq.md +1 -1
- package/src/prompts/contracts/shared_interaction.md +14 -0
- package/src/prompts/system.md +113 -32
- package/src/skills/analysis-campaign/SKILL.md +10 -14
- package/src/skills/baseline/SKILL.md +51 -38
- package/src/skills/baseline/references/baseline-plan-template.md +2 -0
- package/src/skills/decision/SKILL.md +12 -8
- package/src/skills/experiment/SKILL.md +28 -16
- package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +3 -8
- package/src/skills/idea/SKILL.md +18 -8
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/intake-audit/SKILL.md +2 -8
- package/src/skills/rebuttal/SKILL.md +2 -8
- package/src/skills/review/SKILL.md +2 -8
- package/src/skills/scout/SKILL.md +2 -8
- package/src/skills/write/SKILL.md +53 -17
- package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
- package/src/skills/write/templates/README.md +408 -0
- package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/write/templates/aaai2026/README.md +534 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
- package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
- package/src/skills/write/templates/acl/README.md +50 -0
- package/src/skills/write/templates/acl/acl.sty +312 -0
- package/src/skills/write/templates/acl/acl_latex.tex +377 -0
- package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
- package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
- package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
- package/src/skills/write/templates/acl/custom.bib +70 -0
- package/src/skills/write/templates/acl/formatting.md +326 -0
- package/src/skills/write/templates/asplos2027/main.tex +459 -0
- package/src/skills/write/templates/asplos2027/references.bib +135 -0
- package/src/skills/write/templates/colm2025/README.md +3 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
- package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
- package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
- package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
- package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
- package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
- package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
- package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
- package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
- package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
- package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
- package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
- package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
- package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
- package/src/skills/write/templates/neurips2025/Makefile +36 -0
- package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
- package/src/skills/write/templates/neurips2025/main.tex +38 -0
- package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
- package/src/skills/write/templates/nsdi2027/main.tex +426 -0
- package/src/skills/write/templates/nsdi2027/references.bib +151 -0
- package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/osdi2026/main.tex +429 -0
- package/src/skills/write/templates/osdi2026/references.bib +150 -0
- package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/sosp2026/main.tex +532 -0
- package/src/skills/write/templates/sosp2026/references.bib +148 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-BKZ103sn.js} +110 -14
- package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-mTTzGAlK.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-C_wWw4AP.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BH58n3GY.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BKGRUH7e.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-BMADwFWJ.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ZOnTIHLN.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-CQ7h1Djm.js} +830 -86
- package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-GVS5MsnC.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-BZNv1JML.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-TWcJsdQA.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DIjHiR2x.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-D3ooGAH0.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-DfVfE9hN.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DDl0_Mc0.js} +1 -1
- package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-s8JhzuX1.js} +12 -155
- package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-C2Sf6SJM.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-CXFLoIsa.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BYTmz2fK.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-CjWBI1O9.js} +1 -1
- package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-B0Dd8CxK.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-DdOBU3-S.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-B8HGgLwQ.js} +9 -9
- package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-CKaefIN2.js} +1 -1
- package/src/ui/dist/assets/{code-DuMINRsg.js → code-BWAY76JP.js} +1 -1
- package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-C1NwU5oQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-CywslwB9.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-B4kzuOBQ.js} +1 -1
- package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils-H2fjA46S.js} +1 -1
- package/src/ui/dist/assets/{image-DBVGaooo.js → image-D-NZM-6P.js} +1 -1
- package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-7Chr1g9c.js} +3734 -1862
- package/src/ui/dist/assets/{index-DjSFDmgB.js → index-BdM1Gqfr.js} +2 -2
- package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-CDxNdQdz.js} +1 -1
- package/src/ui/dist/assets/{index-Do9N28uB.css → index-DGIYDuTv.css} +163 -34
- package/src/ui/dist/assets/index-DHZJ_0TI.js +159 -0
- package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-BzjLiXir.js} +1 -1
- package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-Cb2uKKe6.js} +1 -1
- package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-Bg72DGgT.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-Ce_0BglY.js} +1 -1
- package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-DPaACDrh.js} +1 -1
- package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-C_mA6R0w.js} +1 -1
- package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BvTgE5__.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-CgPeMOwP.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-xPhz7P5B.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-C3Un3YQr.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-BgxLa0Ri.js} +1 -1
- package/src/ui/dist/index.html +5 -2
- /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
|
@@ -46,8 +46,10 @@ def _infer_stage_from_skill(skill_id: object) -> str | None:
|
|
|
46
46
|
return "baseline"
|
|
47
47
|
if normalized in {"idea", "scout+idea"}:
|
|
48
48
|
return "idea"
|
|
49
|
-
if normalized
|
|
49
|
+
if normalized == "experiment":
|
|
50
50
|
return "experiment"
|
|
51
|
+
if normalized in {"analysis-campaign", "analysis", "analysis_slice"}:
|
|
52
|
+
return "analysis"
|
|
51
53
|
if normalized in {"write", "finalize"}:
|
|
52
54
|
return "writing"
|
|
53
55
|
if normalized == "decision":
|
|
@@ -103,6 +105,56 @@ def _load_artifact_record(path: Path) -> dict[str, Any] | None:
|
|
|
103
105
|
return None
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
def _normalize_paths_map(value: object) -> dict[str, str | None]:
|
|
109
|
+
if not isinstance(value, dict):
|
|
110
|
+
return {}
|
|
111
|
+
normalized: dict[str, str | None] = {}
|
|
112
|
+
for raw_key, raw_value in value.items():
|
|
113
|
+
key = str(raw_key or "").strip()
|
|
114
|
+
if not key:
|
|
115
|
+
continue
|
|
116
|
+
if raw_value is None:
|
|
117
|
+
normalized[key] = None
|
|
118
|
+
continue
|
|
119
|
+
text = str(raw_value).strip()
|
|
120
|
+
normalized[key] = text or None
|
|
121
|
+
return normalized
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _normalize_string_list(value: object) -> list[str]:
|
|
125
|
+
if not isinstance(value, list):
|
|
126
|
+
return []
|
|
127
|
+
items: list[str] = []
|
|
128
|
+
seen: set[str] = set()
|
|
129
|
+
for raw in value:
|
|
130
|
+
text = str(raw or "").strip()
|
|
131
|
+
if not text or text in seen:
|
|
132
|
+
continue
|
|
133
|
+
seen.add(text)
|
|
134
|
+
items.append(text)
|
|
135
|
+
return items
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run_artifact_context(quest_root: Path, run_id: str | None) -> dict[str, Any] | None:
|
|
139
|
+
normalized = str(run_id or "").strip()
|
|
140
|
+
if not normalized:
|
|
141
|
+
return None
|
|
142
|
+
path = quest_root / ".ds" / "runs" / normalized / "artifact.json"
|
|
143
|
+
payload = read_json(path, {})
|
|
144
|
+
if not isinstance(payload, dict) or not payload:
|
|
145
|
+
return None
|
|
146
|
+
record = dict(payload.get("record") or {}) if isinstance(payload.get("record"), dict) else {}
|
|
147
|
+
checkpoint = dict(payload.get("checkpoint") or {}) if isinstance(payload.get("checkpoint"), dict) else {}
|
|
148
|
+
return {
|
|
149
|
+
"path": str(path),
|
|
150
|
+
"record": record,
|
|
151
|
+
"checkpoint": checkpoint,
|
|
152
|
+
"head_commit": str(checkpoint.get("head") or record.get("head_commit") or "").strip() or None,
|
|
153
|
+
"paths_map": _normalize_paths_map(record.get("paths")),
|
|
154
|
+
"changed_files": _normalize_string_list(record.get("files_changed")),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
106
158
|
def _build_run_contexts(quest_root: Path, *, default_branch: str) -> dict[str, dict[str, Any]]:
|
|
107
159
|
contexts: dict[str, dict[str, Any]] = {}
|
|
108
160
|
artifact_roots = [quest_root / "artifacts"]
|
|
@@ -145,6 +197,7 @@ def _build_run_contexts(quest_root: Path, *, default_branch: str) -> dict[str, d
|
|
|
145
197
|
current["worktree_rel_path"] = current.get("worktree_rel_path") or record.get("worktree_rel_path")
|
|
146
198
|
current["summary"] = current.get("summary") or summary
|
|
147
199
|
current["updated_at"] = current.get("updated_at") or updated_at
|
|
200
|
+
current["artifact_path"] = current.get("artifact_path") or str(artifact_path)
|
|
148
201
|
contexts[run_id] = current
|
|
149
202
|
return contexts
|
|
150
203
|
|
|
@@ -160,6 +213,7 @@ def _resolve_entry_context(
|
|
|
160
213
|
raw_event_type = str(entry.get("raw_event_type") or entry.get("kind") or "").strip()
|
|
161
214
|
run_context = run_contexts.get(run_id or "")
|
|
162
215
|
artifact_context: dict[str, Any] | None = None
|
|
216
|
+
artifact_path: str | None = None
|
|
163
217
|
for raw_path in entry.get("paths") or []:
|
|
164
218
|
try:
|
|
165
219
|
path = Path(str(raw_path))
|
|
@@ -169,14 +223,21 @@ def _resolve_entry_context(
|
|
|
169
223
|
continue
|
|
170
224
|
artifact_context = _load_artifact_record(path)
|
|
171
225
|
if artifact_context:
|
|
226
|
+
artifact_path = str(path)
|
|
172
227
|
break
|
|
228
|
+
if run_id is None and artifact_context:
|
|
229
|
+
run_id = str(artifact_context.get("run_id") or "").strip() or None
|
|
230
|
+
run_artifact = _run_artifact_context(quest_root, run_id)
|
|
173
231
|
branch_name = _normalize_branch_name(
|
|
174
|
-
(artifact_context or {}).get("branch")
|
|
232
|
+
(artifact_context or {}).get("branch")
|
|
233
|
+
or (((run_artifact or {}).get("record") or {}) if isinstance((run_artifact or {}).get("record"), dict) else {}).get("branch")
|
|
234
|
+
or (run_context or {}).get("branch_name"),
|
|
175
235
|
fallback=default_branch,
|
|
176
236
|
)
|
|
177
237
|
stage_key = (
|
|
178
238
|
_infer_stage_from_skill(entry.get("skill_id"))
|
|
179
239
|
or _infer_stage_from_artifact(artifact_context or {})
|
|
240
|
+
or _infer_stage_from_artifact((((run_artifact or {}).get("record") or {}) if run_artifact else {}))
|
|
180
241
|
or (run_context or {}).get("stage_key")
|
|
181
242
|
or _infer_stage_from_event_type(raw_event_type)
|
|
182
243
|
or "general"
|
|
@@ -186,9 +247,15 @@ def _resolve_entry_context(
|
|
|
186
247
|
"branch_name": branch_name,
|
|
187
248
|
"stage_key": stage_key,
|
|
188
249
|
"worktree_rel_path": (artifact_context or {}).get("worktree_rel_path")
|
|
250
|
+
or (((run_artifact or {}).get("record") or {}) if run_artifact else {}).get("worktree_rel_path")
|
|
189
251
|
or (run_context or {}).get("worktree_rel_path"),
|
|
252
|
+
"artifact_context": artifact_context,
|
|
253
|
+
"artifact_path": artifact_path or (run_context or {}).get("artifact_path"),
|
|
254
|
+
"run_artifact": run_artifact,
|
|
190
255
|
"trace_confidence": "artifact"
|
|
191
256
|
if artifact_context
|
|
257
|
+
else "run_artifact"
|
|
258
|
+
if run_artifact
|
|
192
259
|
else "run_context"
|
|
193
260
|
if run_context
|
|
194
261
|
else "default_branch",
|
|
@@ -196,6 +263,25 @@ def _resolve_entry_context(
|
|
|
196
263
|
|
|
197
264
|
|
|
198
265
|
def _build_action(entry: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
|
266
|
+
artifact_context = (
|
|
267
|
+
dict(context.get("artifact_context") or {})
|
|
268
|
+
if isinstance(context.get("artifact_context"), dict)
|
|
269
|
+
else {}
|
|
270
|
+
)
|
|
271
|
+
run_artifact = (
|
|
272
|
+
dict(context.get("run_artifact") or {})
|
|
273
|
+
if isinstance(context.get("run_artifact"), dict)
|
|
274
|
+
else {}
|
|
275
|
+
)
|
|
276
|
+
run_record = dict(run_artifact.get("record") or {}) if isinstance(run_artifact.get("record"), dict) else {}
|
|
277
|
+
paths_map = _normalize_paths_map(artifact_context.get("paths")) or _normalize_paths_map(run_record.get("paths"))
|
|
278
|
+
changed_files = _normalize_string_list(artifact_context.get("files_changed")) or _normalize_string_list(
|
|
279
|
+
artifact_context.get("changed_files")
|
|
280
|
+
) or _normalize_string_list(run_record.get("files_changed")) or _normalize_string_list(run_artifact.get("changed_files"))
|
|
281
|
+
details_json = dict(artifact_context.get("details") or {}) if isinstance(artifact_context.get("details"), dict) else {}
|
|
282
|
+
checkpoint_json = dict(run_artifact.get("checkpoint") or {}) if isinstance(run_artifact.get("checkpoint"), dict) else {}
|
|
283
|
+
metadata = dict(entry.get("metadata") or {}) if isinstance(entry.get("metadata"), dict) else {}
|
|
284
|
+
payload_json = artifact_context or metadata or None
|
|
199
285
|
return {
|
|
200
286
|
"action_id": entry.get("id"),
|
|
201
287
|
"kind": entry.get("kind"),
|
|
@@ -217,6 +303,22 @@ def _build_action(entry: dict[str, Any], context: dict[str, Any]) -> dict[str, A
|
|
|
217
303
|
"reason": entry.get("reason"),
|
|
218
304
|
"raw_event_type": entry.get("raw_event_type"),
|
|
219
305
|
"paths": [str(item) for item in (entry.get("paths") or []) if item],
|
|
306
|
+
"paths_map": paths_map or None,
|
|
307
|
+
"artifact_id": artifact_context.get("artifact_id"),
|
|
308
|
+
"artifact_kind": artifact_context.get("kind"),
|
|
309
|
+
"artifact_path": context.get("artifact_path"),
|
|
310
|
+
"head_commit": (
|
|
311
|
+
str(
|
|
312
|
+
artifact_context.get("head_commit")
|
|
313
|
+
or run_artifact.get("head_commit")
|
|
314
|
+
or ""
|
|
315
|
+
).strip()
|
|
316
|
+
or None
|
|
317
|
+
),
|
|
318
|
+
"payload_json": payload_json,
|
|
319
|
+
"details_json": details_json or (metadata or None),
|
|
320
|
+
"checkpoint_json": checkpoint_json or None,
|
|
321
|
+
"changed_files": changed_files or None,
|
|
220
322
|
"trace_confidence": context.get("trace_confidence"),
|
|
221
323
|
}
|
|
222
324
|
|
|
@@ -277,6 +379,24 @@ def _build_trace_item(
|
|
|
277
379
|
) or None
|
|
278
380
|
run_ids = sorted({str(item.get("run_id") or "").strip() for item in ordered_actions if item.get("run_id")})
|
|
279
381
|
skill_ids = sorted({str(item.get("skill_id") or "").strip() for item in ordered_actions if item.get("skill_id")})
|
|
382
|
+
primary_action = next(
|
|
383
|
+
(
|
|
384
|
+
action
|
|
385
|
+
for action in reversed(ordered_actions)
|
|
386
|
+
if action.get("artifact_id")
|
|
387
|
+
or action.get("head_commit")
|
|
388
|
+
or action.get("payload_json")
|
|
389
|
+
or action.get("details_json")
|
|
390
|
+
),
|
|
391
|
+
ordered_actions[-1] if ordered_actions else {},
|
|
392
|
+
)
|
|
393
|
+
merged_paths_map: dict[str, str | None] = {}
|
|
394
|
+
for action in ordered_actions:
|
|
395
|
+
if isinstance(action.get("paths_map"), dict):
|
|
396
|
+
merged_paths_map.update(action.get("paths_map") or {})
|
|
397
|
+
merged_changed_files = _normalize_string_list(
|
|
398
|
+
[item for action in ordered_actions for item in (action.get("changed_files") or [])]
|
|
399
|
+
)
|
|
280
400
|
return {
|
|
281
401
|
"selection_type": selection_type,
|
|
282
402
|
"selection_ref": selection_ref,
|
|
@@ -291,6 +411,13 @@ def _build_trace_item(
|
|
|
291
411
|
"counts": _build_counts(ordered_actions),
|
|
292
412
|
"run_ids": run_ids,
|
|
293
413
|
"skill_ids": skill_ids,
|
|
414
|
+
"artifact_id": primary_action.get("artifact_id"),
|
|
415
|
+
"artifact_kind": primary_action.get("artifact_kind"),
|
|
416
|
+
"head_commit": primary_action.get("head_commit"),
|
|
417
|
+
"payload_json": primary_action.get("payload_json"),
|
|
418
|
+
"details_json": primary_action.get("details_json"),
|
|
419
|
+
"paths_map": merged_paths_map or None,
|
|
420
|
+
"changed_files": merged_changed_files or None,
|
|
294
421
|
"actions": ordered_actions,
|
|
295
422
|
}
|
|
296
423
|
|
|
@@ -17,11 +17,12 @@ try:
|
|
|
17
17
|
except ImportError: # pragma: no cover
|
|
18
18
|
fcntl = None
|
|
19
19
|
|
|
20
|
-
from ..artifact.metrics import build_metrics_timeline,
|
|
20
|
+
from ..artifact.metrics import build_metrics_timeline, extract_latest_metric
|
|
21
21
|
from ..config import ConfigManager
|
|
22
22
|
from ..connector_runtime import conversation_identity_key, normalize_conversation_id
|
|
23
23
|
from ..gitops import current_branch, export_git_graph, head_commit, init_repo
|
|
24
24
|
from ..home import repo_root
|
|
25
|
+
from ..registries import BaselineRegistry
|
|
25
26
|
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
26
27
|
from ..skills import SkillInstaller
|
|
27
28
|
from ..web_search import extract_web_search_payload
|
|
@@ -48,6 +49,7 @@ class QuestService:
|
|
|
48
49
|
self.home = home
|
|
49
50
|
self.quests_root = home / "quests"
|
|
50
51
|
self.skill_installer = skill_installer
|
|
52
|
+
self.baseline_registry = BaselineRegistry(home)
|
|
51
53
|
self._file_cache_lock = threading.Lock()
|
|
52
54
|
self._file_cache: dict[str, dict[str, Any]] = {}
|
|
53
55
|
self._jsonl_cache_lock = threading.Lock()
|
|
@@ -116,6 +118,10 @@ class QuestService:
|
|
|
116
118
|
def _research_state_path(quest_root: Path) -> Path:
|
|
117
119
|
return quest_root / ".ds" / "research_state.json"
|
|
118
120
|
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _lab_canvas_state_path(quest_root: Path) -> Path:
|
|
123
|
+
return quest_root / ".ds" / "lab_canvas_state.json"
|
|
124
|
+
|
|
119
125
|
def _default_research_state(self, quest_root: Path) -> dict[str, Any]:
|
|
120
126
|
return {
|
|
121
127
|
"version": 1,
|
|
@@ -129,12 +135,27 @@ class QuestService:
|
|
|
129
135
|
"active_analysis_campaign_id": None,
|
|
130
136
|
"analysis_parent_branch": None,
|
|
131
137
|
"analysis_parent_worktree_root": None,
|
|
138
|
+
"paper_parent_branch": None,
|
|
139
|
+
"paper_parent_worktree_root": None,
|
|
140
|
+
"paper_parent_run_id": None,
|
|
132
141
|
"next_pending_slice_id": None,
|
|
133
142
|
"workspace_mode": "quest",
|
|
134
143
|
"last_flow_type": None,
|
|
135
144
|
"updated_at": utc_now(),
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
def _default_lab_canvas_state(self, quest_root: Path) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"version": 1,
|
|
150
|
+
"layout_json": {
|
|
151
|
+
"branch": {},
|
|
152
|
+
"event": {},
|
|
153
|
+
"stage": {},
|
|
154
|
+
"preferences": {},
|
|
155
|
+
},
|
|
156
|
+
"updated_at": utc_now(),
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
def read_research_state(self, quest_root: Path) -> dict[str, Any]:
|
|
139
160
|
self._initialize_runtime_files(quest_root)
|
|
140
161
|
defaults = self._default_research_state(quest_root)
|
|
@@ -151,6 +172,9 @@ class QuestService:
|
|
|
151
172
|
parent_root = str(merged.get("analysis_parent_worktree_root") or "").strip()
|
|
152
173
|
if parent_root and not Path(parent_root).exists():
|
|
153
174
|
merged["analysis_parent_worktree_root"] = None
|
|
175
|
+
paper_parent_root = str(merged.get("paper_parent_worktree_root") or "").strip()
|
|
176
|
+
if paper_parent_root and not Path(paper_parent_root).exists():
|
|
177
|
+
merged["paper_parent_worktree_root"] = None
|
|
154
178
|
return merged
|
|
155
179
|
|
|
156
180
|
def write_research_state(self, quest_root: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -166,6 +190,39 @@ class QuestService:
|
|
|
166
190
|
current[key] = str(value) if isinstance(value, Path) else value
|
|
167
191
|
return self.write_research_state(quest_root, current)
|
|
168
192
|
|
|
193
|
+
def read_lab_canvas_state(self, quest_root: Path) -> dict[str, Any]:
|
|
194
|
+
self._initialize_runtime_files(quest_root)
|
|
195
|
+
defaults = self._default_lab_canvas_state(quest_root)
|
|
196
|
+
payload = self._read_cached_json(self._lab_canvas_state_path(quest_root), defaults)
|
|
197
|
+
if not isinstance(payload, dict):
|
|
198
|
+
payload = defaults
|
|
199
|
+
merged = {**defaults, **payload}
|
|
200
|
+
layout_json = dict(merged.get("layout_json") or {}) if isinstance(merged.get("layout_json"), dict) else {}
|
|
201
|
+
for key in ("branch", "event", "stage", "preferences"):
|
|
202
|
+
if not isinstance(layout_json.get(key), dict):
|
|
203
|
+
layout_json[key] = {}
|
|
204
|
+
merged["layout_json"] = layout_json
|
|
205
|
+
return merged
|
|
206
|
+
|
|
207
|
+
def update_lab_canvas_state(
|
|
208
|
+
self,
|
|
209
|
+
quest_root: Path,
|
|
210
|
+
*,
|
|
211
|
+
layout_json: dict[str, Any] | None = None,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
current = self.read_lab_canvas_state(quest_root)
|
|
214
|
+
normalized_layout = dict(layout_json or {}) if isinstance(layout_json, dict) else {}
|
|
215
|
+
for key in ("branch", "event", "stage", "preferences"):
|
|
216
|
+
if not isinstance(normalized_layout.get(key), dict):
|
|
217
|
+
normalized_layout[key] = {}
|
|
218
|
+
payload = {
|
|
219
|
+
**current,
|
|
220
|
+
"layout_json": normalized_layout,
|
|
221
|
+
"updated_at": utc_now(),
|
|
222
|
+
}
|
|
223
|
+
write_json(self._lab_canvas_state_path(quest_root), payload)
|
|
224
|
+
return payload
|
|
225
|
+
|
|
169
226
|
def workspace_roots(self, quest_root: Path) -> list[Path]:
|
|
170
227
|
roots: list[Path] = [quest_root]
|
|
171
228
|
state = self.read_research_state(quest_root)
|
|
@@ -206,9 +263,37 @@ class QuestService:
|
|
|
206
263
|
def _artifact_roots(self, quest_root: Path) -> list[Path]:
|
|
207
264
|
return [root for root in self.workspace_roots(quest_root) if (root / "artifacts").exists()]
|
|
208
265
|
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _artifact_item_identity(path: Path, payload: dict[str, Any], *, kind: str) -> str:
|
|
268
|
+
normalized_kind = str(kind or payload.get("kind") or path.parent.name or "artifact").strip() or "artifact"
|
|
269
|
+
artifact_id = str(payload.get("artifact_id") or payload.get("id") or "").strip()
|
|
270
|
+
if artifact_id:
|
|
271
|
+
return f"{normalized_kind}:artifact:{artifact_id}"
|
|
272
|
+
branch_name = str(payload.get("branch") or "").strip()
|
|
273
|
+
run_id = str(payload.get("run_id") or "").strip()
|
|
274
|
+
if normalized_kind == "runs" and run_id and branch_name:
|
|
275
|
+
return f"{normalized_kind}:branch_run:{branch_name}:{run_id}"
|
|
276
|
+
if normalized_kind == "runs" and run_id:
|
|
277
|
+
return f"{normalized_kind}:run:{run_id}"
|
|
278
|
+
idea_id = str(payload.get("idea_id") or "").strip()
|
|
279
|
+
if normalized_kind == "ideas" and idea_id and branch_name:
|
|
280
|
+
return f"{normalized_kind}:branch_idea:{branch_name}:{idea_id}"
|
|
281
|
+
if normalized_kind == "ideas" and idea_id:
|
|
282
|
+
return f"{normalized_kind}:idea:{idea_id}"
|
|
283
|
+
return f"path:{path.resolve()}"
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _artifact_item_rank(payload: dict[str, Any], *, path: Path, mtime_ns: int) -> tuple[str, str, int, int, str]:
|
|
287
|
+
return (
|
|
288
|
+
str(payload.get("updated_at") or ""),
|
|
289
|
+
str(payload.get("created_at") or ""),
|
|
290
|
+
len(payload),
|
|
291
|
+
mtime_ns,
|
|
292
|
+
str(path),
|
|
293
|
+
)
|
|
294
|
+
|
|
209
295
|
def _collect_artifacts(self, quest_root: Path) -> list[dict[str, Any]]:
|
|
210
|
-
|
|
211
|
-
seen_paths: set[str] = set()
|
|
296
|
+
artifacts_by_identity: dict[str, dict[str, Any]] = {}
|
|
212
297
|
for root in self._artifact_roots(quest_root):
|
|
213
298
|
artifacts_root = root / "artifacts"
|
|
214
299
|
if not artifacts_root.exists():
|
|
@@ -217,19 +302,37 @@ class QuestService:
|
|
|
217
302
|
if not folder.is_dir():
|
|
218
303
|
continue
|
|
219
304
|
for path in sorted(folder.glob("*.json")):
|
|
220
|
-
resolved_key = str(path.resolve())
|
|
221
|
-
if resolved_key in seen_paths:
|
|
222
|
-
continue
|
|
223
|
-
seen_paths.add(resolved_key)
|
|
224
305
|
item = self._read_cached_json(path, {})
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
306
|
+
payload = item if isinstance(item, dict) else {}
|
|
307
|
+
try:
|
|
308
|
+
mtime_ns = path.stat().st_mtime_ns
|
|
309
|
+
except OSError:
|
|
310
|
+
mtime_ns = 0
|
|
311
|
+
artifact = {
|
|
312
|
+
"kind": folder.name,
|
|
313
|
+
"path": str(path),
|
|
314
|
+
"payload": item,
|
|
315
|
+
"workspace_root": str(root),
|
|
316
|
+
}
|
|
317
|
+
identity = self._artifact_item_identity(path, payload, kind=folder.name)
|
|
318
|
+
existing = artifacts_by_identity.get(identity)
|
|
319
|
+
existing_payload = existing.get("payload") if isinstance((existing or {}).get("payload"), dict) else {}
|
|
320
|
+
existing_path = Path(str((existing or {}).get("path") or path))
|
|
321
|
+
try:
|
|
322
|
+
existing_mtime_ns = existing_path.stat().st_mtime_ns if existing else 0
|
|
323
|
+
except OSError:
|
|
324
|
+
existing_mtime_ns = 0
|
|
325
|
+
if existing is None or self._artifact_item_rank(
|
|
326
|
+
payload,
|
|
327
|
+
path=path,
|
|
328
|
+
mtime_ns=mtime_ns,
|
|
329
|
+
) >= self._artifact_item_rank(
|
|
330
|
+
existing_payload,
|
|
331
|
+
path=existing_path,
|
|
332
|
+
mtime_ns=existing_mtime_ns,
|
|
333
|
+
):
|
|
334
|
+
artifacts_by_identity[identity] = artifact
|
|
335
|
+
artifacts = list(artifacts_by_identity.values())
|
|
233
336
|
artifacts.sort(
|
|
234
337
|
key=lambda item: str(
|
|
235
338
|
((item.get("payload") or {}).get("updated_at"))
|
|
@@ -253,6 +356,9 @@ class QuestService:
|
|
|
253
356
|
continue
|
|
254
357
|
seen_paths.add(key)
|
|
255
358
|
payload = self._read_cached_yaml(path, {})
|
|
359
|
+
baseline_id = str(payload.get("source_baseline_id") or "").strip() if isinstance(payload, dict) else ""
|
|
360
|
+
if baseline_id and self.baseline_registry.is_deleted(baseline_id):
|
|
361
|
+
continue
|
|
256
362
|
if isinstance(payload, dict) and payload:
|
|
257
363
|
attachments.append(payload)
|
|
258
364
|
if not attachments:
|
|
@@ -267,22 +373,7 @@ class QuestService:
|
|
|
267
373
|
|
|
268
374
|
@staticmethod
|
|
269
375
|
def _latest_metric_from_payload(payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
270
|
-
|
|
271
|
-
if not metrics_summary:
|
|
272
|
-
return None
|
|
273
|
-
progress_eval = payload.get("progress_eval") if isinstance(payload.get("progress_eval"), dict) else {}
|
|
274
|
-
comparisons = payload.get("baseline_comparisons") if isinstance(payload.get("baseline_comparisons"), dict) else {}
|
|
275
|
-
primary_metric_id = (
|
|
276
|
-
str(progress_eval.get("primary_metric_id") or comparisons.get("primary_metric_id") or "").strip()
|
|
277
|
-
or next(iter(metrics_summary.keys()))
|
|
278
|
-
)
|
|
279
|
-
result = {
|
|
280
|
-
"key": primary_metric_id,
|
|
281
|
-
"value": metrics_summary.get(primary_metric_id),
|
|
282
|
-
}
|
|
283
|
-
if progress_eval.get("delta_vs_baseline") is not None:
|
|
284
|
-
result["delta_vs_baseline"] = progress_eval.get("delta_vs_baseline")
|
|
285
|
-
return result
|
|
376
|
+
return extract_latest_metric(payload)
|
|
286
377
|
|
|
287
378
|
@staticmethod
|
|
288
379
|
def _parse_numeric_quest_id(value: str | None) -> int | None:
|
|
@@ -980,6 +1071,9 @@ class QuestService:
|
|
|
980
1071
|
"active_analysis_campaign_id": research_state.get("active_analysis_campaign_id"),
|
|
981
1072
|
"analysis_parent_branch": research_state.get("analysis_parent_branch"),
|
|
982
1073
|
"analysis_parent_worktree_root": research_state.get("analysis_parent_worktree_root"),
|
|
1074
|
+
"paper_parent_branch": research_state.get("paper_parent_branch"),
|
|
1075
|
+
"paper_parent_worktree_root": research_state.get("paper_parent_worktree_root"),
|
|
1076
|
+
"paper_parent_run_id": research_state.get("paper_parent_run_id"),
|
|
983
1077
|
"next_pending_slice_id": research_state.get("next_pending_slice_id"),
|
|
984
1078
|
"workspace_mode": research_state.get("workspace_mode") or "quest",
|
|
985
1079
|
"active_baseline_id": active_baseline_id,
|
|
@@ -1981,7 +2075,11 @@ class QuestService:
|
|
|
1981
2075
|
},
|
|
1982
2076
|
}
|
|
1983
2077
|
|
|
1984
|
-
resolution_root =
|
|
2078
|
+
resolution_root = (
|
|
2079
|
+
quest_root
|
|
2080
|
+
if document_id.startswith(("questpath::", "memory::"))
|
|
2081
|
+
else workspace_root
|
|
2082
|
+
)
|
|
1985
2083
|
path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
|
|
1986
2084
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
1987
2085
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
@@ -2162,11 +2260,16 @@ class QuestService:
|
|
|
2162
2260
|
asset_name = f"{safe_stem}-{generate_id('asset').split('-', 1)[1]}{asset_suffix}"
|
|
2163
2261
|
asset_relative_dir = self._markdown_asset_directory(base_relative)
|
|
2164
2262
|
asset_relative = (asset_relative_dir / asset_name).as_posix()
|
|
2165
|
-
asset_root =
|
|
2263
|
+
asset_root = (
|
|
2264
|
+
quest_root
|
|
2265
|
+
if document_id.startswith(("questpath::", "memory::"))
|
|
2266
|
+
else workspace_root
|
|
2267
|
+
)
|
|
2166
2268
|
asset_path = resolve_within(asset_root, asset_relative)
|
|
2167
2269
|
ensure_dir(asset_path.parent)
|
|
2168
2270
|
asset_path.write_bytes(content)
|
|
2169
|
-
|
|
2271
|
+
asset_document_scope = "questpath" if document_id.startswith(("questpath::", "memory::")) else "path"
|
|
2272
|
+
asset_document_id = f"{asset_document_scope}::{asset_relative}"
|
|
2170
2273
|
relative_markdown_path = self._relative_path_from_base(base_relative, asset_relative)
|
|
2171
2274
|
return {
|
|
2172
2275
|
"ok": True,
|
|
@@ -2368,6 +2471,9 @@ class QuestService:
|
|
|
2368
2471
|
research_state_path = self._research_state_path(quest_root)
|
|
2369
2472
|
if not research_state_path.exists():
|
|
2370
2473
|
write_json(research_state_path, self._default_research_state(quest_root))
|
|
2474
|
+
lab_canvas_state_path = self._lab_canvas_state_path(quest_root)
|
|
2475
|
+
if not lab_canvas_state_path.exists():
|
|
2476
|
+
write_json(lab_canvas_state_path, self._default_lab_canvas_state(quest_root))
|
|
2371
2477
|
agent_status_path = self._agent_status_path(quest_root)
|
|
2372
2478
|
if not agent_status_path.exists():
|
|
2373
2479
|
write_json(agent_status_path, self._default_agent_status(quest_root))
|