@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
|
@@ -14,7 +14,7 @@ from ... import __version__ as DEEPSCIENTIST_VERSION
|
|
|
14
14
|
from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, list_branch_canvas, log_ref_history
|
|
15
15
|
from ...memory import MemoryService
|
|
16
16
|
from ...quest import QuestService
|
|
17
|
-
from ...shared import generate_id, read_text, resolve_within, sha256_text, utc_now
|
|
17
|
+
from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
|
|
18
18
|
from ...runners import RunRequest
|
|
19
19
|
|
|
20
20
|
|
|
@@ -171,9 +171,9 @@ npm --prefix src/ui run build</pre>
|
|
|
171
171
|
|
|
172
172
|
def cli_health(self) -> dict:
|
|
173
173
|
online_channels = [
|
|
174
|
-
|
|
175
|
-
for
|
|
176
|
-
if
|
|
174
|
+
snapshot
|
|
175
|
+
for snapshot in self.app.list_connector_statuses()
|
|
176
|
+
if snapshot.get("enabled") is not False
|
|
177
177
|
]
|
|
178
178
|
return {
|
|
179
179
|
"status": "ok",
|
|
@@ -211,6 +211,14 @@ npm --prefix src/ui run build</pre>
|
|
|
211
211
|
def baselines(self) -> list[dict]:
|
|
212
212
|
return self.app.artifact_service.baselines.list_entries()
|
|
213
213
|
|
|
214
|
+
def baseline_delete(self, baseline_id: str) -> dict | tuple[int, dict]:
|
|
215
|
+
try:
|
|
216
|
+
return self.app.artifact_service.delete_baseline(baseline_id)
|
|
217
|
+
except FileNotFoundError as exc:
|
|
218
|
+
return 404, {"ok": False, "message": str(exc), "baseline_id": baseline_id}
|
|
219
|
+
except ValueError as exc:
|
|
220
|
+
return 400, {"ok": False, "message": str(exc), "baseline_id": baseline_id}
|
|
221
|
+
|
|
214
222
|
def qq_bindings(self) -> list[dict]:
|
|
215
223
|
return self.app.list_qq_bindings()
|
|
216
224
|
|
|
@@ -248,12 +256,15 @@ npm --prefix src/ui run build</pre>
|
|
|
248
256
|
else []
|
|
249
257
|
)
|
|
250
258
|
force_connector_rebind_raw = body.get("force_connector_rebind")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
if force_connector_rebind_raw is None:
|
|
260
|
+
force_connector_rebind = True
|
|
261
|
+
else:
|
|
262
|
+
force_connector_rebind = bool(force_connector_rebind_raw) and str(force_connector_rebind_raw).strip().lower() not in {
|
|
263
|
+
"0",
|
|
264
|
+
"false",
|
|
265
|
+
"no",
|
|
266
|
+
"off",
|
|
267
|
+
}
|
|
257
268
|
requested_baseline_ref = body.get("requested_baseline_ref")
|
|
258
269
|
startup_contract = body.get("startup_contract")
|
|
259
270
|
auto_start = body.get("auto_start") is True
|
|
@@ -645,8 +656,6 @@ npm --prefix src/ui run build</pre>
|
|
|
645
656
|
session = self.app.bash_exec_service.get_session(quest_root, session_id)
|
|
646
657
|
except FileNotFoundError:
|
|
647
658
|
return 404, {"ok": False, "message": f"Unknown terminal session `{session_id}`."}
|
|
648
|
-
if str(session.get("kind") or "").lower() != "terminal":
|
|
649
|
-
return 400, {"ok": False, "message": "not_terminal_session"}
|
|
650
659
|
if str(session.get("status") or "").lower() in {"completed", "failed", "terminated"}:
|
|
651
660
|
return 409, {"ok": False, "message": "terminal_session_inactive", "session": session}
|
|
652
661
|
try:
|
|
@@ -720,6 +729,28 @@ npm --prefix src/ui run build</pre>
|
|
|
720
729
|
def workflow(self, quest_id: str) -> dict:
|
|
721
730
|
return self.app.quest_service.workflow(quest_id)
|
|
722
731
|
|
|
732
|
+
def quest_layout(self, quest_id: str) -> dict:
|
|
733
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
734
|
+
payload = self.app.quest_service.read_lab_canvas_state(quest_root)
|
|
735
|
+
return {
|
|
736
|
+
"layout_json": payload.get("layout_json") if isinstance(payload.get("layout_json"), dict) else {},
|
|
737
|
+
"updated_at": payload.get("updated_at"),
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
def quest_layout_update(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
|
|
741
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
742
|
+
raw_layout = body.get("layout_json")
|
|
743
|
+
if raw_layout is not None and not isinstance(raw_layout, dict):
|
|
744
|
+
return 400, {"ok": False, "message": "`layout_json` must be an object."}
|
|
745
|
+
payload = self.app.quest_service.update_lab_canvas_state(
|
|
746
|
+
quest_root,
|
|
747
|
+
layout_json=dict(raw_layout or {}),
|
|
748
|
+
)
|
|
749
|
+
return {
|
|
750
|
+
"layout_json": payload.get("layout_json") if isinstance(payload.get("layout_json"), dict) else {},
|
|
751
|
+
"updated_at": payload.get("updated_at"),
|
|
752
|
+
}
|
|
753
|
+
|
|
723
754
|
def node_traces(self, quest_id: str, path: str) -> dict:
|
|
724
755
|
query = self.parse_query(path)
|
|
725
756
|
selection_type = ((query.get("selection_type") or [""])[0] or "").strip() or None
|
|
@@ -747,6 +778,20 @@ npm --prefix src/ui run build</pre>
|
|
|
747
778
|
def git_branches(self, quest_id: str) -> dict:
|
|
748
779
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
749
780
|
payload = list_branch_canvas(quest_root, quest_id=quest_id)
|
|
781
|
+
research_state = self.app.quest_service.read_research_state(quest_root)
|
|
782
|
+
active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
|
|
783
|
+
research_head_branch = str(research_state.get("research_head_branch") or "").strip() or None
|
|
784
|
+
payload["active_workspace_ref"] = active_workspace_branch
|
|
785
|
+
payload["research_head_ref"] = research_head_branch
|
|
786
|
+
payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
|
|
787
|
+
quest_data = self.app.quest_service.read_quest_yaml(quest_root)
|
|
788
|
+
active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
|
|
789
|
+
active_analysis_campaign_id = str(research_state.get("active_analysis_campaign_id") or "").strip() or None
|
|
790
|
+
current_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
|
|
791
|
+
workspace_mode = str(research_state.get("workspace_mode") or "").strip().lower() or "quest"
|
|
792
|
+
paper_parent_branch = str(research_state.get("paper_parent_branch") or "").strip() or None
|
|
793
|
+
paper_parent_run_id = str(research_state.get("paper_parent_run_id") or "").strip() or None
|
|
794
|
+
next_pending_slice_id = str(research_state.get("next_pending_slice_id") or "").strip() or None
|
|
750
795
|
try:
|
|
751
796
|
branch_summary = self.app.artifact_service.list_research_branches(quest_root)
|
|
752
797
|
except Exception:
|
|
@@ -756,11 +801,104 @@ npm --prefix src/ui run build</pre>
|
|
|
756
801
|
for item in (branch_summary.get("branches") or [])
|
|
757
802
|
if str(item.get("branch_name") or "").strip()
|
|
758
803
|
}
|
|
804
|
+
active_campaign = {}
|
|
805
|
+
if active_analysis_campaign_id:
|
|
806
|
+
try:
|
|
807
|
+
active_campaign = self.app.artifact_service.get_analysis_campaign(
|
|
808
|
+
quest_root,
|
|
809
|
+
campaign_id=active_analysis_campaign_id,
|
|
810
|
+
)
|
|
811
|
+
except Exception:
|
|
812
|
+
active_campaign = {}
|
|
813
|
+
campaign_parent_branch = (
|
|
814
|
+
str(active_campaign.get("parent_branch") or "").strip() or None
|
|
815
|
+
if isinstance(active_campaign, dict)
|
|
816
|
+
else None
|
|
817
|
+
)
|
|
818
|
+
campaign_slices = [
|
|
819
|
+
dict(item)
|
|
820
|
+
for item in ((active_campaign or {}).get("slices") or [])
|
|
821
|
+
if isinstance(item, dict)
|
|
822
|
+
]
|
|
823
|
+
campaign_total_slices = len(campaign_slices)
|
|
824
|
+
campaign_completed_slices = sum(
|
|
825
|
+
1 for item in campaign_slices if str(item.get("status") or "").strip().lower() == "completed"
|
|
826
|
+
)
|
|
827
|
+
slice_by_branch = {
|
|
828
|
+
str(item.get("branch") or "").strip(): item
|
|
829
|
+
for item in campaign_slices
|
|
830
|
+
if str(item.get("branch") or "").strip()
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
def resolve_workflow_state(ref: str, summary: dict[str, object] | None) -> dict[str, object]:
|
|
834
|
+
branch_kind = self.app.artifact_service._branch_kind_from_name(ref)
|
|
835
|
+
has_main_result = bool((summary or {}).get("has_main_result"))
|
|
836
|
+
workflow_state: dict[str, object] = {
|
|
837
|
+
"analysis_state": "none",
|
|
838
|
+
"writing_state": "not_ready",
|
|
839
|
+
"analysis_campaign_id": active_analysis_campaign_id,
|
|
840
|
+
"total_slices": campaign_total_slices or None,
|
|
841
|
+
"completed_slices": campaign_completed_slices or None,
|
|
842
|
+
"next_pending_slice_id": next_pending_slice_id,
|
|
843
|
+
"paper_parent_branch": paper_parent_branch,
|
|
844
|
+
"paper_parent_run_id": paper_parent_run_id,
|
|
845
|
+
"status_reason": None,
|
|
846
|
+
}
|
|
847
|
+
if branch_kind == "analysis":
|
|
848
|
+
slice_entry = slice_by_branch.get(ref)
|
|
849
|
+
slice_status = str((slice_entry or {}).get("status") or "pending").strip().lower() or "pending"
|
|
850
|
+
if slice_status == "completed":
|
|
851
|
+
workflow_state["analysis_state"] = "completed"
|
|
852
|
+
workflow_state["status_reason"] = "Analysis slice completed."
|
|
853
|
+
elif ref == current_workspace_branch or str((slice_entry or {}).get("slice_id") or "").strip() == next_pending_slice_id:
|
|
854
|
+
workflow_state["analysis_state"] = "active"
|
|
855
|
+
workflow_state["status_reason"] = (
|
|
856
|
+
f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
|
|
857
|
+
if campaign_total_slices
|
|
858
|
+
else "Analysis slice active."
|
|
859
|
+
)
|
|
860
|
+
else:
|
|
861
|
+
workflow_state["analysis_state"] = "pending"
|
|
862
|
+
workflow_state["status_reason"] = "Analysis slice pending."
|
|
863
|
+
return workflow_state
|
|
864
|
+
if branch_kind == "paper":
|
|
865
|
+
if ref == current_workspace_branch and workspace_mode == "paper":
|
|
866
|
+
workflow_state["writing_state"] = "completed" if active_anchor == "finalize" else "active"
|
|
867
|
+
workflow_state["status_reason"] = (
|
|
868
|
+
"Writing finalized." if active_anchor == "finalize" else "Writing workspace active."
|
|
869
|
+
)
|
|
870
|
+
else:
|
|
871
|
+
workflow_state["writing_state"] = "ready"
|
|
872
|
+
workflow_state["status_reason"] = "Writing workspace prepared."
|
|
873
|
+
return workflow_state
|
|
874
|
+
if campaign_parent_branch and ref == campaign_parent_branch:
|
|
875
|
+
workflow_state["analysis_state"] = "completed" if next_pending_slice_id is None else "active"
|
|
876
|
+
if has_main_result:
|
|
877
|
+
workflow_state["writing_state"] = "ready" if next_pending_slice_id is None else "blocked_by_analysis"
|
|
878
|
+
workflow_state["status_reason"] = (
|
|
879
|
+
"Analysis complete. Ready for writing."
|
|
880
|
+
if next_pending_slice_id is None
|
|
881
|
+
else (
|
|
882
|
+
f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
|
|
883
|
+
+ (f" · next: {next_pending_slice_id}" if next_pending_slice_id else "")
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
return workflow_state
|
|
887
|
+
if has_main_result:
|
|
888
|
+
workflow_state["writing_state"] = "ready"
|
|
889
|
+
workflow_state["status_reason"] = "Main experiment recorded. Ready for writing."
|
|
890
|
+
return workflow_state
|
|
891
|
+
workflow_state["status_reason"] = "Awaiting main experiment result."
|
|
892
|
+
return workflow_state
|
|
893
|
+
|
|
759
894
|
for node in payload.get("nodes", []):
|
|
760
895
|
ref = str(node.get("ref") or "").strip()
|
|
761
896
|
if not ref:
|
|
762
897
|
continue
|
|
763
898
|
summary = branch_summary_by_name.get(ref)
|
|
899
|
+
node["active_workspace"] = ref == active_workspace_branch
|
|
900
|
+
node["research_head"] = ref == research_head_branch
|
|
901
|
+
node["workflow_state"] = resolve_workflow_state(ref, summary if isinstance(summary, dict) else None)
|
|
764
902
|
if not isinstance(summary, dict):
|
|
765
903
|
continue
|
|
766
904
|
node["branch_no"] = summary.get("branch_no")
|
|
@@ -775,6 +913,7 @@ npm --prefix src/ui run build</pre>
|
|
|
775
913
|
node["idea_draft_path"] = summary.get("idea_draft_path")
|
|
776
914
|
node["latest_main_experiment"] = summary.get("latest_main_experiment")
|
|
777
915
|
node["experiment_count"] = summary.get("experiment_count")
|
|
916
|
+
node["has_main_result"] = summary.get("has_main_result")
|
|
778
917
|
return payload
|
|
779
918
|
|
|
780
919
|
def git_log(self, quest_id: str, path: str) -> dict:
|
|
@@ -818,6 +957,126 @@ npm --prefix src/ui run build</pre>
|
|
|
818
957
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
819
958
|
return diff_file_between_refs(quest_root, base=base, head=head, path=file_path)
|
|
820
959
|
|
|
960
|
+
def file_change_diff(self, quest_id: str, path: str) -> dict:
|
|
961
|
+
query = self.parse_query(path)
|
|
962
|
+
run_id = ((query.get("run_id") or [""])[0] or "").strip()
|
|
963
|
+
event_id = ((query.get("event_id") or [""])[0] or "").strip() or None
|
|
964
|
+
raw_path = ((query.get("path") or [""])[0] or "").strip()
|
|
965
|
+
if not run_id or not raw_path:
|
|
966
|
+
return self._file_change_diff_unavailable(
|
|
967
|
+
raw_path=raw_path,
|
|
968
|
+
run_id=run_id or None,
|
|
969
|
+
event_id=event_id,
|
|
970
|
+
message="`run_id` and `path` are required.",
|
|
971
|
+
ok=False,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
975
|
+
run_artifact = read_json(quest_root / ".ds" / "runs" / run_id / "artifact.json", default={})
|
|
976
|
+
if not isinstance(run_artifact, dict):
|
|
977
|
+
return self._file_change_diff_unavailable(
|
|
978
|
+
raw_path=raw_path,
|
|
979
|
+
run_id=run_id,
|
|
980
|
+
event_id=event_id,
|
|
981
|
+
message="Historical patch unavailable. Run artifact metadata is missing.",
|
|
982
|
+
ok=False,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
record = run_artifact.get("record") if isinstance(run_artifact.get("record"), dict) else {}
|
|
986
|
+
checkpoint = run_artifact.get("checkpoint") if isinstance(run_artifact.get("checkpoint"), dict) else {}
|
|
987
|
+
base = str(record.get("head_commit") or "").strip()
|
|
988
|
+
head = str(checkpoint.get("head") or "").strip()
|
|
989
|
+
branch = str(record.get("branch") or "").strip() or None
|
|
990
|
+
workspace_root = self._file_change_workspace_root(run_artifact, record)
|
|
991
|
+
relative_path = None
|
|
992
|
+
display_path = None
|
|
993
|
+
|
|
994
|
+
if not base or not head:
|
|
995
|
+
relative_path = self._relative_file_change_path(None, raw_path, workspace_root)
|
|
996
|
+
display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
|
|
997
|
+
return self._file_change_diff_unavailable(
|
|
998
|
+
raw_path=raw_path,
|
|
999
|
+
run_id=run_id,
|
|
1000
|
+
event_id=event_id,
|
|
1001
|
+
base=base,
|
|
1002
|
+
head=head,
|
|
1003
|
+
branch=branch,
|
|
1004
|
+
relative_path=relative_path,
|
|
1005
|
+
display_path=display_path,
|
|
1006
|
+
message="Historical patch unavailable. Run artifact metadata is missing the recorded commit range.",
|
|
1007
|
+
ok=True,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
repo_root = self._resolve_git_repo_root_for_file_change(raw_path, workspace_root)
|
|
1011
|
+
relative_path = self._relative_file_change_path(repo_root, raw_path, workspace_root)
|
|
1012
|
+
display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
|
|
1013
|
+
|
|
1014
|
+
if repo_root is None or not relative_path:
|
|
1015
|
+
return self._file_change_diff_unavailable(
|
|
1016
|
+
raw_path=raw_path,
|
|
1017
|
+
run_id=run_id,
|
|
1018
|
+
event_id=event_id,
|
|
1019
|
+
base=base,
|
|
1020
|
+
head=head,
|
|
1021
|
+
branch=branch,
|
|
1022
|
+
relative_path=relative_path,
|
|
1023
|
+
display_path=display_path,
|
|
1024
|
+
message="Historical patch unavailable. DeepScientist could not map this file to a git worktree.",
|
|
1025
|
+
ok=True,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
try:
|
|
1029
|
+
diff = diff_file_between_refs(repo_root, base=base, head=head, path=relative_path)
|
|
1030
|
+
except Exception:
|
|
1031
|
+
return self._file_change_diff_unavailable(
|
|
1032
|
+
raw_path=raw_path,
|
|
1033
|
+
run_id=run_id,
|
|
1034
|
+
event_id=event_id,
|
|
1035
|
+
base=base,
|
|
1036
|
+
head=head,
|
|
1037
|
+
branch=branch,
|
|
1038
|
+
relative_path=relative_path,
|
|
1039
|
+
display_path=display_path,
|
|
1040
|
+
message=(
|
|
1041
|
+
"Historical patch unavailable. The saved run checkpoint does not match the git repository "
|
|
1042
|
+
"that currently owns this file."
|
|
1043
|
+
),
|
|
1044
|
+
ok=True,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
if (
|
|
1048
|
+
not diff.get("binary")
|
|
1049
|
+
and not diff.get("lines")
|
|
1050
|
+
and int(diff.get("added") or 0) == 0
|
|
1051
|
+
and int(diff.get("removed") or 0) == 0
|
|
1052
|
+
):
|
|
1053
|
+
return self._file_change_diff_unavailable(
|
|
1054
|
+
raw_path=raw_path,
|
|
1055
|
+
run_id=run_id,
|
|
1056
|
+
event_id=event_id,
|
|
1057
|
+
base=base,
|
|
1058
|
+
head=head,
|
|
1059
|
+
branch=branch,
|
|
1060
|
+
relative_path=relative_path,
|
|
1061
|
+
display_path=display_path,
|
|
1062
|
+
message=(
|
|
1063
|
+
"Historical patch unavailable. This event only preserved file-level metadata or the edit "
|
|
1064
|
+
"did not land in the run's final checkpoint."
|
|
1065
|
+
),
|
|
1066
|
+
ok=True,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
**diff,
|
|
1071
|
+
"available": True,
|
|
1072
|
+
"source": "run_range",
|
|
1073
|
+
"display_path": display_path or relative_path,
|
|
1074
|
+
"run_id": run_id,
|
|
1075
|
+
"event_id": event_id,
|
|
1076
|
+
"branch": branch,
|
|
1077
|
+
"message": None,
|
|
1078
|
+
}
|
|
1079
|
+
|
|
821
1080
|
def git_commit_file(self, quest_id: str, path: str) -> dict:
|
|
822
1081
|
query = self.parse_query(path)
|
|
823
1082
|
sha = ((query.get("sha") or [""])[0] or "").strip()
|
|
@@ -1207,6 +1466,16 @@ npm --prefix src/ui run build</pre>
|
|
|
1207
1466
|
"ok": False,
|
|
1208
1467
|
"message": str(exc),
|
|
1209
1468
|
}
|
|
1469
|
+
raw_reasoning_effort = (
|
|
1470
|
+
body.get("model_reasoning_effort")
|
|
1471
|
+
if "model_reasoning_effort" in body
|
|
1472
|
+
else runner_cfg.get("model_reasoning_effort")
|
|
1473
|
+
)
|
|
1474
|
+
reasoning_effort = (
|
|
1475
|
+
str(raw_reasoning_effort).strip()
|
|
1476
|
+
if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
|
|
1477
|
+
else ("xhigh" if raw_reasoning_effort is None else None)
|
|
1478
|
+
)
|
|
1210
1479
|
request = RunRequest(
|
|
1211
1480
|
quest_id=quest_id,
|
|
1212
1481
|
quest_root=quest_root,
|
|
@@ -1218,8 +1487,7 @@ npm --prefix src/ui run build</pre>
|
|
|
1218
1487
|
approval_policy=runner_cfg.get("approval_policy", "on-request"),
|
|
1219
1488
|
sandbox_mode=runner_cfg.get("sandbox_mode", "workspace-write"),
|
|
1220
1489
|
turn_reason=body.get("turn_reason") or "user_message",
|
|
1221
|
-
reasoning_effort=
|
|
1222
|
-
or runner_cfg.get("model_reasoning_effort", "xhigh"),
|
|
1490
|
+
reasoning_effort=reasoning_effort,
|
|
1223
1491
|
)
|
|
1224
1492
|
result = runner.run(request)
|
|
1225
1493
|
if result.output_text:
|
|
@@ -1355,6 +1623,8 @@ npm --prefix src/ui run build</pre>
|
|
|
1355
1623
|
result = self.app.config_manager.save_named_payload(name, body["structured"])
|
|
1356
1624
|
else:
|
|
1357
1625
|
result = self.app.config_manager.save_named_text(name, body.get("content", ""))
|
|
1626
|
+
if result.get("ok") and name == "config":
|
|
1627
|
+
result["runtime_reload"] = self.app.reload_runtime_config()
|
|
1358
1628
|
if result.get("ok") and name == "connectors":
|
|
1359
1629
|
result["runtime_reload"] = self.app.reload_connectors_config()
|
|
1360
1630
|
if result.get("ok") and name == "runners":
|
|
@@ -1411,6 +1681,136 @@ npm --prefix src/ui run build</pre>
|
|
|
1411
1681
|
return {}
|
|
1412
1682
|
return json.loads(raw.decode("utf-8"))
|
|
1413
1683
|
|
|
1684
|
+
@staticmethod
|
|
1685
|
+
def _file_change_workspace_root(run_artifact: dict, record: dict) -> Path | None:
|
|
1686
|
+
workspace_root = str(run_artifact.get("workspace_root") or record.get("workspace_root") or "").strip()
|
|
1687
|
+
if not workspace_root:
|
|
1688
|
+
return None
|
|
1689
|
+
return Path(workspace_root).expanduser()
|
|
1690
|
+
|
|
1691
|
+
@staticmethod
|
|
1692
|
+
def _file_change_path_candidates(raw_path: str, workspace_root: Path | None) -> list[Path]:
|
|
1693
|
+
text = str(raw_path or "").strip()
|
|
1694
|
+
if not text:
|
|
1695
|
+
return []
|
|
1696
|
+
raw_candidate = Path(text).expanduser()
|
|
1697
|
+
candidates: list[Path] = []
|
|
1698
|
+
if raw_candidate.is_absolute():
|
|
1699
|
+
candidates.append(raw_candidate)
|
|
1700
|
+
elif workspace_root is not None:
|
|
1701
|
+
candidates.append(workspace_root / raw_candidate)
|
|
1702
|
+
else:
|
|
1703
|
+
candidates.append(raw_candidate)
|
|
1704
|
+
if workspace_root is not None:
|
|
1705
|
+
candidates.append(workspace_root)
|
|
1706
|
+
|
|
1707
|
+
resolved: list[Path] = []
|
|
1708
|
+
seen: set[str] = set()
|
|
1709
|
+
for candidate in candidates:
|
|
1710
|
+
for variant in (candidate, candidate.resolve(strict=False)):
|
|
1711
|
+
key = str(variant)
|
|
1712
|
+
if key in seen:
|
|
1713
|
+
continue
|
|
1714
|
+
seen.add(key)
|
|
1715
|
+
resolved.append(variant)
|
|
1716
|
+
return resolved
|
|
1717
|
+
|
|
1718
|
+
@staticmethod
|
|
1719
|
+
def _git_probe_root(candidate: Path) -> Path:
|
|
1720
|
+
if candidate.exists():
|
|
1721
|
+
return candidate if candidate.is_dir() else candidate.parent
|
|
1722
|
+
for parent in candidate.parents:
|
|
1723
|
+
if parent.exists():
|
|
1724
|
+
return parent
|
|
1725
|
+
return candidate.parent
|
|
1726
|
+
|
|
1727
|
+
def _resolve_git_repo_root_for_file_change(self, raw_path: str, workspace_root: Path | None) -> Path | None:
|
|
1728
|
+
for candidate in self._file_change_path_candidates(raw_path, workspace_root):
|
|
1729
|
+
probe_root = self._git_probe_root(candidate)
|
|
1730
|
+
if not str(probe_root).strip():
|
|
1731
|
+
continue
|
|
1732
|
+
result = run_command(["git", "rev-parse", "--show-toplevel"], cwd=probe_root, check=False)
|
|
1733
|
+
if result.returncode != 0:
|
|
1734
|
+
continue
|
|
1735
|
+
top_level = result.stdout.strip()
|
|
1736
|
+
if top_level:
|
|
1737
|
+
return Path(top_level).resolve()
|
|
1738
|
+
return None
|
|
1739
|
+
|
|
1740
|
+
def _relative_file_change_path(self, repo_root: Path | None, raw_path: str, workspace_root: Path | None) -> str | None:
|
|
1741
|
+
if repo_root is None:
|
|
1742
|
+
if Path(str(raw_path or "").strip()).is_absolute():
|
|
1743
|
+
return None
|
|
1744
|
+
normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
|
|
1745
|
+
return normalized or None
|
|
1746
|
+
|
|
1747
|
+
repo_root_resolved = repo_root.resolve()
|
|
1748
|
+
for candidate in self._file_change_path_candidates(raw_path, workspace_root):
|
|
1749
|
+
try:
|
|
1750
|
+
return candidate.relative_to(repo_root_resolved).as_posix()
|
|
1751
|
+
except ValueError:
|
|
1752
|
+
continue
|
|
1753
|
+
if Path(str(raw_path or "").strip()).is_absolute():
|
|
1754
|
+
return None
|
|
1755
|
+
normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
|
|
1756
|
+
return normalized or None
|
|
1757
|
+
|
|
1758
|
+
@staticmethod
|
|
1759
|
+
def _display_file_change_path(quest_root: Path, raw_path: str, *, relative_path: str | None = None) -> str:
|
|
1760
|
+
quest_root_resolved = quest_root.resolve()
|
|
1761
|
+
for candidate in [Path(str(raw_path or "").strip()).expanduser()]:
|
|
1762
|
+
for variant in (candidate, candidate.resolve(strict=False)):
|
|
1763
|
+
try:
|
|
1764
|
+
relative = variant.relative_to(quest_root_resolved)
|
|
1765
|
+
except ValueError:
|
|
1766
|
+
continue
|
|
1767
|
+
parts = relative.parts
|
|
1768
|
+
if len(parts) >= 3 and parts[0] == ".ds" and parts[1] == "worktrees":
|
|
1769
|
+
branch_root = parts[2]
|
|
1770
|
+
remainder = Path(*parts[3:]).as_posix() if len(parts) > 3 else ""
|
|
1771
|
+
return f"{branch_root}/{remainder}" if remainder else branch_root
|
|
1772
|
+
return relative.as_posix()
|
|
1773
|
+
if relative_path:
|
|
1774
|
+
return relative_path
|
|
1775
|
+
text = str(raw_path or "").strip()
|
|
1776
|
+
return Path(text).name or text
|
|
1777
|
+
|
|
1778
|
+
@staticmethod
|
|
1779
|
+
def _file_change_diff_unavailable(
|
|
1780
|
+
*,
|
|
1781
|
+
raw_path: str,
|
|
1782
|
+
run_id: str | None,
|
|
1783
|
+
event_id: str | None,
|
|
1784
|
+
message: str,
|
|
1785
|
+
ok: bool,
|
|
1786
|
+
base: str = "",
|
|
1787
|
+
head: str = "",
|
|
1788
|
+
branch: str | None = None,
|
|
1789
|
+
relative_path: str | None = None,
|
|
1790
|
+
display_path: str | None = None,
|
|
1791
|
+
) -> dict:
|
|
1792
|
+
normalized_path = relative_path or str(raw_path or "").strip()
|
|
1793
|
+
return {
|
|
1794
|
+
"ok": ok,
|
|
1795
|
+
"available": False,
|
|
1796
|
+
"source": "unavailable",
|
|
1797
|
+
"run_id": run_id,
|
|
1798
|
+
"event_id": event_id,
|
|
1799
|
+
"branch": branch,
|
|
1800
|
+
"display_path": display_path or normalized_path,
|
|
1801
|
+
"base": base,
|
|
1802
|
+
"head": head,
|
|
1803
|
+
"path": normalized_path,
|
|
1804
|
+
"old_path": None,
|
|
1805
|
+
"status": "modified",
|
|
1806
|
+
"binary": False,
|
|
1807
|
+
"added": 0,
|
|
1808
|
+
"removed": 0,
|
|
1809
|
+
"lines": [],
|
|
1810
|
+
"truncated": False,
|
|
1811
|
+
"message": message,
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1414
1814
|
@staticmethod
|
|
1415
1815
|
def _markdown_title(path: Path) -> str:
|
|
1416
1816
|
content = read_text(path)
|
|
@@ -21,6 +21,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
21
21
|
("GET", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/bindings$"), "connector_bindings"),
|
|
22
22
|
("POST", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/inbound$"), "connector_inbound"),
|
|
23
23
|
("GET", re.compile(r"^/api/baselines$"), "baselines"),
|
|
24
|
+
("DELETE", re.compile(r"^/api/baselines/(?P<baseline_id>[^/]+)$"), "baseline_delete"),
|
|
24
25
|
("GET", re.compile(r"^/api/quests$"), "quests"),
|
|
25
26
|
("GET", re.compile(r"^/api/quest-id/next$"), "quest_next_id"),
|
|
26
27
|
("POST", re.compile(r"^/api/quests$"), "quest_create"),
|
|
@@ -35,6 +36,8 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
35
36
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/events$"), "quest_events"),
|
|
36
37
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/artifacts$"), "quest_artifacts"),
|
|
37
38
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/workflow$"), "workflow"),
|
|
39
|
+
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/layout$"), "quest_layout"),
|
|
40
|
+
("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/layout$"), "quest_layout_update"),
|
|
38
41
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/node-traces$"), "node_traces"),
|
|
39
42
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/node-traces/(?P<node_ref>.+)$"), "node_trace"),
|
|
40
43
|
("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/stage-view$"), "stage_view"),
|
|
@@ -60,6 +63,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
60
63
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),
|
|
61
64
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/diff-file$"), "git_diff_file"),
|
|
62
65
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit-file$"), "git_commit_file"),
|
|
66
|
+
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/operations/file-change-diff$"), "file_change_diff"),
|
|
63
67
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/runs$"), "runs"),
|
|
64
68
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/memory$"), "quest_memory"),
|
|
65
69
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents$"), "documents"),
|