@researai/deepscientist 1.5.13 → 1.5.15
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/README.md +8 -0
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +134 -49
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/README.md +8 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/README.md +8 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4004 -538
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +79 -64
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/config/models.py +6 -3
- package/src/deepscientist/config/service.py +7 -2
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +75 -4
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +869 -236
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/diff.py +167 -1
- package/src/deepscientist/mcp/server.py +331 -21
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +275 -491
- package/src/deepscientist/quest/service.py +2336 -145
- package/src/deepscientist/quest/stage_views.py +305 -29
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +88 -5
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +984 -1985
- package/src/skills/analysis-campaign/SKILL.md +31 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +267 -994
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/skills/decision/SKILL.md +19 -2
- package/src/skills/experiment/SKILL.md +8 -2
- package/src/skills/finalize/SKILL.md +18 -0
- package/src/skills/idea/SKILL.md +78 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/optimize/SKILL.md +1644 -0
- package/src/skills/rebuttal/SKILL.md +2 -1
- package/src/skills/review/SKILL.md +2 -1
- package/src/skills/write/SKILL.md +80 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +1445 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-CLQauncb.js → index-D_E4281X.js} +5418 -28620
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-DnYB3xb1.js} +12 -12
- package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1 -1
- package/src/ui/dist/assets/CliPlugin-CB1YODQn.js +0 -5905
|
@@ -16,6 +16,7 @@ STANDARD_SKILLS = (
|
|
|
16
16
|
"scout",
|
|
17
17
|
"baseline",
|
|
18
18
|
"idea",
|
|
19
|
+
"optimize",
|
|
19
20
|
"experiment",
|
|
20
21
|
"analysis-campaign",
|
|
21
22
|
"write",
|
|
@@ -43,6 +44,10 @@ STAGE_MEMORY_PLAN = {
|
|
|
43
44
|
"quest": ("papers", "ideas", "decisions", "knowledge"),
|
|
44
45
|
"global": ("papers", "knowledge", "templates"),
|
|
45
46
|
},
|
|
47
|
+
"optimize": {
|
|
48
|
+
"quest": ("episodes", "decisions", "ideas", "knowledge"),
|
|
49
|
+
"global": ("knowledge", "templates"),
|
|
50
|
+
},
|
|
46
51
|
"experiment": {
|
|
47
52
|
"quest": ("ideas", "decisions", "episodes", "knowledge"),
|
|
48
53
|
"global": ("knowledge", "templates"),
|
|
@@ -66,6 +71,38 @@ STAGE_MEMORY_PLAN = {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
def classify_turn_intent(user_message: str) -> str:
|
|
75
|
+
text = str(user_message or "").strip()
|
|
76
|
+
if not text:
|
|
77
|
+
return "continue_stage"
|
|
78
|
+
normalized = " ".join(text.split()).lower()
|
|
79
|
+
structured_bootstrap_markers = (
|
|
80
|
+
"project bootstrap",
|
|
81
|
+
"primary research request",
|
|
82
|
+
"research goals",
|
|
83
|
+
"baseline context",
|
|
84
|
+
"reference papers",
|
|
85
|
+
"operational constraints",
|
|
86
|
+
"research delivery mode",
|
|
87
|
+
"decision handling mode",
|
|
88
|
+
"launch mode",
|
|
89
|
+
"research contract",
|
|
90
|
+
"mandatory working rules",
|
|
91
|
+
)
|
|
92
|
+
structured_hit_count = sum(1 for marker in structured_bootstrap_markers if marker in normalized)
|
|
93
|
+
if structured_hit_count >= 2:
|
|
94
|
+
return "continue_stage"
|
|
95
|
+
if normalized.startswith("/new ") or normalized.startswith("/new\n"):
|
|
96
|
+
return "continue_stage"
|
|
97
|
+
question_markers = ["?", "?", "现在进展", "全局", "多久", "什么情况", "在哪", "在哪里", "how long", "what", "where"]
|
|
98
|
+
if any(marker in normalized for marker in question_markers):
|
|
99
|
+
return "answer_user_question_first"
|
|
100
|
+
command_markers = ["继续", "发给我", "发送", "运行", "启动", "resume", "send", "run", "launch"]
|
|
101
|
+
if any(marker in normalized for marker in command_markers):
|
|
102
|
+
return "execute_user_command_first"
|
|
103
|
+
return "continue_stage"
|
|
104
|
+
|
|
105
|
+
|
|
69
106
|
class PromptBuilder:
|
|
70
107
|
def __init__(self, repo_root: Path, home: Path) -> None:
|
|
71
108
|
self.repo_root = repo_root
|
|
@@ -83,6 +120,8 @@ class PromptBuilder:
|
|
|
83
120
|
user_message: str,
|
|
84
121
|
model: str,
|
|
85
122
|
turn_reason: str = "user_message",
|
|
123
|
+
turn_intent: str | None = None,
|
|
124
|
+
turn_mode: str | None = None,
|
|
86
125
|
retry_context: dict | None = None,
|
|
87
126
|
) -> str:
|
|
88
127
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
@@ -151,7 +190,12 @@ class PromptBuilder:
|
|
|
151
190
|
[
|
|
152
191
|
"",
|
|
153
192
|
"## Turn Driver",
|
|
154
|
-
self._turn_driver_block(
|
|
193
|
+
self._turn_driver_block(
|
|
194
|
+
turn_reason=turn_reason,
|
|
195
|
+
user_message=user_message,
|
|
196
|
+
turn_intent=turn_intent,
|
|
197
|
+
turn_mode=turn_mode,
|
|
198
|
+
),
|
|
155
199
|
"",
|
|
156
200
|
"## Continuation Guard",
|
|
157
201
|
self._continuation_guard_block(
|
|
@@ -173,12 +217,18 @@ class PromptBuilder:
|
|
|
173
217
|
"## Research Delivery Policy",
|
|
174
218
|
self._research_delivery_policy_block(snapshot),
|
|
175
219
|
"",
|
|
220
|
+
"## Optimization Frontier Snapshot",
|
|
221
|
+
self._optimization_frontier_block(snapshot, quest_root),
|
|
222
|
+
"",
|
|
176
223
|
"## Paper And Evidence Snapshot",
|
|
177
224
|
self._paper_and_evidence_block(snapshot, quest_root),
|
|
178
225
|
"",
|
|
179
226
|
"## Retry Recovery Packet",
|
|
180
227
|
self._retry_recovery_block(retry_context),
|
|
181
228
|
"",
|
|
229
|
+
"## Recovery Resume Packet",
|
|
230
|
+
self._recovery_resume_block(snapshot=snapshot, turn_reason=turn_reason),
|
|
231
|
+
"",
|
|
182
232
|
"## Interaction Style",
|
|
183
233
|
self._interaction_style_block(default_locale=default_locale, user_message=user_message, snapshot=snapshot),
|
|
184
234
|
"",
|
|
@@ -206,7 +256,14 @@ class PromptBuilder:
|
|
|
206
256
|
)
|
|
207
257
|
return "\n\n".join(sections).strip() + "\n"
|
|
208
258
|
|
|
209
|
-
def _turn_driver_block(
|
|
259
|
+
def _turn_driver_block(
|
|
260
|
+
self,
|
|
261
|
+
*,
|
|
262
|
+
turn_reason: str,
|
|
263
|
+
user_message: str,
|
|
264
|
+
turn_intent: str | None = None,
|
|
265
|
+
turn_mode: str | None = None,
|
|
266
|
+
) -> str:
|
|
210
267
|
normalized_reason = str(turn_reason or "user_message").strip() or "user_message"
|
|
211
268
|
lines = [f"- turn_reason: {normalized_reason}"]
|
|
212
269
|
if normalized_reason == "auto_continue":
|
|
@@ -228,9 +285,28 @@ class PromptBuilder:
|
|
|
228
285
|
preview = " ".join(str(user_message or "").split())
|
|
229
286
|
if len(preview) > 220:
|
|
230
287
|
preview = preview[:217].rstrip() + "..."
|
|
288
|
+
resolved_turn_intent = str(turn_intent or self._turn_intent(user_message)).strip() or "continue_stage"
|
|
289
|
+
resolved_turn_mode = str(turn_mode or "stage_execution").strip() or "stage_execution"
|
|
290
|
+
lines.append(f"- turn_intent: {resolved_turn_intent}")
|
|
291
|
+
lines.append(f"- turn_mode: {resolved_turn_mode}")
|
|
292
|
+
if resolved_turn_intent == "answer_user_question_first":
|
|
293
|
+
lines.append(
|
|
294
|
+
"- answer_first_rule: the user primarily asked a direct question. Answer it in plain language before resuming any background stage work or generating new route artifacts."
|
|
295
|
+
)
|
|
296
|
+
lines.append(
|
|
297
|
+
"- direct_answer_tool_rule: if the question is about overall progress, paper readiness, current best result, or next step, call artifact.get_global_status(detail='brief'|'full', locale='zh'|'en') before answering from memory or local stage context."
|
|
298
|
+
)
|
|
299
|
+
elif resolved_turn_intent == "execute_user_command_first":
|
|
300
|
+
lines.append(
|
|
301
|
+
"- command_first_rule: the user primarily gave a concrete instruction. Execute or acknowledge that instruction first before resuming background stage narration."
|
|
302
|
+
)
|
|
231
303
|
lines.append(f"- direct_user_message_preview: {preview or 'none'}")
|
|
232
304
|
return "\n".join(lines)
|
|
233
305
|
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _turn_intent(user_message: str) -> str:
|
|
308
|
+
return classify_turn_intent(user_message)
|
|
309
|
+
|
|
234
310
|
def _active_communication_surface_block(
|
|
235
311
|
self,
|
|
236
312
|
*,
|
|
@@ -245,8 +321,6 @@ class PromptBuilder:
|
|
|
245
321
|
connector = surface_context["active_connector"]
|
|
246
322
|
chat_type = surface_context["active_chat_type"]
|
|
247
323
|
chat_id = surface_context["active_chat_id"]
|
|
248
|
-
qq_config = connectors_config.get("qq") if isinstance(connectors_config.get("qq"), dict) else {}
|
|
249
|
-
|
|
250
324
|
lines = [
|
|
251
325
|
f"- latest_user_source: {source}",
|
|
252
326
|
f"- active_surface: {surface}",
|
|
@@ -264,38 +338,16 @@ class PromptBuilder:
|
|
|
264
338
|
lines.extend(
|
|
265
339
|
[
|
|
266
340
|
"- qq_surface_rule: QQ is a milestone-report surface, not a full artifact browser.",
|
|
267
|
-
"-
|
|
268
|
-
"- qq_detail_rule:
|
|
269
|
-
"- qq_length_rule: for ordinary QQ progress replies, normally use only 2 to 4 short sentences, or 3 very short bullets at most.",
|
|
270
|
-
"- qq_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action; do not make the user reverse-engineer the status from telemetry.",
|
|
271
|
-
"- qq_internal_signal_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless that detail is necessary for a user decision or to explain a real risk.",
|
|
272
|
-
"- qq_translation_rule: translate internal actions into user value, for example say that you organized the baseline record for easier comparison later instead of listing the files you touched.",
|
|
273
|
-
"- 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, next step, or next update; if the runtime is uncertain, say that directly and still give the next check-in window.",
|
|
274
|
-
f"- qq_auto_send_main_experiment_png: {bool(qq_config.get('auto_send_main_experiment_png', True))}",
|
|
275
|
-
f"- qq_auto_send_analysis_summary_png: {bool(qq_config.get('auto_send_analysis_summary_png', True))}",
|
|
276
|
-
f"- qq_auto_send_slice_png: {bool(qq_config.get('auto_send_slice_png', False))}",
|
|
277
|
-
f"- qq_auto_send_paper_pdf: {bool(qq_config.get('auto_send_paper_pdf', True))}",
|
|
278
|
-
f"- qq_enable_markdown_send: {bool(qq_config.get('enable_markdown_send', False))}",
|
|
279
|
-
f"- qq_enable_file_upload_experimental: {bool(qq_config.get('enable_file_upload_experimental', False))}",
|
|
280
|
-
"- qq_visual_rule: follow the fixed Morandi palette guide defined in the system prompt and active stage skill; do not assume per-install palette config exists.",
|
|
281
|
-
"- qq_media_rule: auto-send only high-value milestone media such as a main-experiment summary PNG, an aggregated analysis summary PNG, or the final paper PDF when available and configured.",
|
|
282
|
-
"- qq_media_rule_2: do not auto-send every slice image, every debug plot, or draft paper figures unless the user explicitly asked for them.",
|
|
283
|
-
"- qq_structured_delivery_rule: when you want native QQ markdown or native QQ image/file delivery, request it through artifact.interact(connector_hints=..., attachments=[...]) instead of inventing connector-specific inline tag syntax.",
|
|
341
|
+
"- qq_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
342
|
+
"- qq_detail_rule: rely on the QQ connector contract for detailed surface formatting instead of expanding it here.",
|
|
284
343
|
]
|
|
285
344
|
)
|
|
286
345
|
elif connector == "weixin":
|
|
287
346
|
lines.extend(
|
|
288
347
|
[
|
|
289
348
|
"- weixin_surface_rule: Weixin is a concise operator surface, not a full artifact browser.",
|
|
290
|
-
"-
|
|
291
|
-
"-
|
|
292
|
-
"- weixin_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action.",
|
|
293
|
-
"- weixin_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete next step explicit whenever possible.",
|
|
294
|
-
"- weixin_eta_rule: for important long-running phases, include a rough ETA or next check-in window when it is helpful and defensible.",
|
|
295
|
-
"- weixin_internal_detail_rule: do not proactively dump file inventories, path lists, retry counters, or monitor-log style telemetry unless the user asked for them or they explain a real risk.",
|
|
296
|
-
"- weixin_context_token_rule: reply continuity is managed by the runtime through `context_token`; do not invent your own reply token scheme.",
|
|
297
|
-
"- weixin_media_rule: when you want native Weixin image, video, or file delivery, request it through artifact.interact(..., attachments=[...]) with `connector_delivery={'weixin': {'media_kind': ...}}` instead of inventing connector-specific inline tag syntax.",
|
|
298
|
-
"- weixin_inbound_media_rule: inbound Weixin image, video, and file messages can arrive as quest-local attachments under `userfiles/weixin/...`; read those files when the user sent media.",
|
|
349
|
+
"- weixin_reply_rule: keep outbound replies concise, respectful, text-first, and progress-aware.",
|
|
350
|
+
"- weixin_detail_rule: rely on the Weixin connector contract for detailed transport formatting instead of expanding it here.",
|
|
299
351
|
]
|
|
300
352
|
)
|
|
301
353
|
else:
|
|
@@ -472,7 +524,20 @@ class PromptBuilder:
|
|
|
472
524
|
pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
|
|
473
525
|
if pending_user_count > 0:
|
|
474
526
|
return f"Poll artifact.interact(...) and handle the {pending_user_count} queued user message(s) first."
|
|
527
|
+
continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
|
|
528
|
+
continuation_anchor = str(snapshot.get("continuation_anchor") or "").strip()
|
|
529
|
+
if continuation_policy == "wait_for_user_or_resume":
|
|
530
|
+
if continuation_anchor:
|
|
531
|
+
return (
|
|
532
|
+
f"The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or "
|
|
533
|
+
f"`/resume`, then continue from `{continuation_anchor}` instead of auto-continuing the previous stage."
|
|
534
|
+
)
|
|
535
|
+
return "The quest is intentionally parked after the latest durable checkpoint. Wait for a new user message or `/resume`."
|
|
536
|
+
if continuation_policy == "none":
|
|
537
|
+
return "Do not auto-continue this quest. Wait for an explicit new user instruction before doing more work."
|
|
475
538
|
active_anchor = str(snapshot.get("active_anchor") or "decision").strip() or "decision"
|
|
539
|
+
if continuation_anchor:
|
|
540
|
+
active_anchor = continuation_anchor
|
|
476
541
|
active_idea_id = str(snapshot.get("active_idea_id") or "").strip()
|
|
477
542
|
next_slice_id = str(snapshot.get("next_pending_slice_id") or "").strip()
|
|
478
543
|
active_campaign_id = str(snapshot.get("active_analysis_campaign_id") or "").strip()
|
|
@@ -489,6 +554,8 @@ class PromptBuilder:
|
|
|
489
554
|
"Continue idea analysis and route selection until the next durable idea branch is submitted "
|
|
490
555
|
"with `lineage_intent='continue_line'` or `lineage_intent='branch_alternative'`."
|
|
491
556
|
)
|
|
557
|
+
if active_anchor == "optimize":
|
|
558
|
+
return "Continue the optimization loop from the current frontier, candidate pool, durable runs, and branch state."
|
|
492
559
|
if active_anchor == "experiment":
|
|
493
560
|
return "Continue the main experiment workflow from the current workspace, logs, and recorded evidence."
|
|
494
561
|
if active_anchor == "analysis-campaign":
|
|
@@ -634,6 +701,24 @@ class PromptBuilder:
|
|
|
634
701
|
|
|
635
702
|
return "\n".join(lines)
|
|
636
703
|
|
|
704
|
+
@staticmethod
|
|
705
|
+
def _recovery_resume_block(*, snapshot: dict, turn_reason: str) -> str:
|
|
706
|
+
if str(turn_reason or "").strip() != "auto_continue":
|
|
707
|
+
return "- none"
|
|
708
|
+
source = str(snapshot.get("last_resume_source") or "").strip()
|
|
709
|
+
if not source.startswith("auto:daemon-recovery"):
|
|
710
|
+
return "- none"
|
|
711
|
+
lines = [
|
|
712
|
+
f"- resume_source: {source}",
|
|
713
|
+
f"- resumed_at: {snapshot.get('last_resume_at') or 'unknown'}",
|
|
714
|
+
f"- abandoned_run_id: {snapshot.get('last_recovery_abandoned_run_id') or 'none'}",
|
|
715
|
+
f"- recovery_summary: {snapshot.get('last_recovery_summary') or 'none'}",
|
|
716
|
+
"- recovery_rule: this turn exists because the daemon/runtime previously died or stale running state was reconciled; first re-establish the current truth before continuing any old stage loop.",
|
|
717
|
+
"- recovery_rule_2: if there is any new user message, handle that before blindly resuming the older subtask.",
|
|
718
|
+
"- recovery_rule_3: do not assume the previous branch-local route is still the right immediate action until branch/workspace, run state, and user intent are checked together.",
|
|
719
|
+
]
|
|
720
|
+
return "\n".join(lines)
|
|
721
|
+
|
|
637
722
|
def _prompt_fragment(self, relative_path: str | Path, *, quest_root: Path | None = None) -> str:
|
|
638
723
|
path = self._prompt_path(relative_path, quest_root=quest_root)
|
|
639
724
|
return self._markdown_body(path)
|
|
@@ -693,6 +778,15 @@ class PromptBuilder:
|
|
|
693
778
|
return value
|
|
694
779
|
return "standard"
|
|
695
780
|
|
|
781
|
+
@staticmethod
|
|
782
|
+
def _standard_profile(snapshot: dict) -> str:
|
|
783
|
+
startup_contract = snapshot.get("startup_contract")
|
|
784
|
+
if isinstance(startup_contract, dict):
|
|
785
|
+
value = str(startup_contract.get("standard_profile") or "").strip().lower()
|
|
786
|
+
if value in {"canonical_research_graph", "optimization_task"}:
|
|
787
|
+
return value
|
|
788
|
+
return "canonical_research_graph"
|
|
789
|
+
|
|
696
790
|
@staticmethod
|
|
697
791
|
def _custom_profile(snapshot: dict) -> str:
|
|
698
792
|
startup_contract = snapshot.get("startup_contract")
|
|
@@ -732,6 +826,7 @@ class PromptBuilder:
|
|
|
732
826
|
def _research_delivery_policy_block(self, snapshot: dict) -> str:
|
|
733
827
|
need_research_paper = self._need_research_paper(snapshot)
|
|
734
828
|
launch_mode = self._launch_mode(snapshot)
|
|
829
|
+
standard_profile = self._standard_profile(snapshot)
|
|
735
830
|
custom_profile = self._custom_profile(snapshot)
|
|
736
831
|
baseline_execution_policy = self._baseline_execution_policy(snapshot)
|
|
737
832
|
review_followup_policy = self._review_followup_policy(snapshot)
|
|
@@ -739,15 +834,14 @@ class PromptBuilder:
|
|
|
739
834
|
lines = [
|
|
740
835
|
f"- need_research_paper: {need_research_paper}",
|
|
741
836
|
f"- launch_mode: {launch_mode}",
|
|
837
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
742
838
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
743
839
|
f"- review_followup_policy: {review_followup_policy if custom_profile == 'review_audit' else 'n/a'}",
|
|
744
840
|
f"- baseline_execution_policy: {baseline_execution_policy if launch_mode == 'custom' else 'n/a'}",
|
|
745
841
|
f"- manuscript_edit_mode: {manuscript_edit_mode if custom_profile in {'review_audit', 'revision_rebuttal'} else 'n/a'}",
|
|
746
842
|
f"- delivery_mode: {'paper_required' if need_research_paper else 'algorithm_first'}",
|
|
843
|
+
"- requested_skill_rule: stage-specific execution detail lives in the requested skill; this block only adds runtime launch policy.",
|
|
747
844
|
"- idea_stage_rule: every accepted idea submission should normally create a new branch/worktree and a new user-visible research node.",
|
|
748
|
-
"- idea_draft_rule: before `artifact.submit_idea(...)`, first finish a concise durable Markdown draft for the chosen route; keep `idea.md` compact and `draft.md` richer.",
|
|
749
|
-
"- idea_literature_floor_rule: before writing or submitting a final selected idea, durably survey at least 5 and usually 5 to 10 related and usable papers; prioritize direct task-modeling or mechanism-neighbor work and only backfill with the closest adjacent translatable papers when necessary.",
|
|
750
|
-
"- idea_reference_rule: the final selected-idea draft should use one consistent standard citation format and include a `References` or `Bibliography` section for the survey-stage papers that actually shaped the motivation, mechanism, or claim boundary.",
|
|
751
845
|
"- lineage_rule: normal idea routing uses exactly two lineage intents: `continue_line` creates a child of the current active branch; `branch_alternative` creates a sibling-like branch from the current branch's parent foundation.",
|
|
752
846
|
"- revise_rule: `artifact.submit_idea(mode='revise', ...)` is maintenance-only compatibility for the same branch and should not be the default research-route mechanism.",
|
|
753
847
|
"- post_main_result_rule: after every `artifact.record_main_experiment(...)`, first interpret the measured result and only then choose the next route.",
|
|
@@ -835,17 +929,21 @@ class PromptBuilder:
|
|
|
835
929
|
"- manuscript_edit_rule: when manuscript revision is needed, provide section-level copy-ready replacement text and explicit deltas even if no LaTeX source is available.",
|
|
836
930
|
]
|
|
837
931
|
)
|
|
932
|
+
elif standard_profile == "optimization_task":
|
|
933
|
+
lines.extend(
|
|
934
|
+
[
|
|
935
|
+
"- standard_optimization_entry_rule: this standard entry is explicitly optimization-only; treat repeated implementation attempts and measured main-experiment results as the primary progress loop.",
|
|
936
|
+
"- standard_optimization_no_analysis_default: do not route into `analysis-campaign` by default; only run extra analysis when it directly validates a suspected win, disambiguates a frontier decision, or exposes a concrete failure mode that changes the next optimization move.",
|
|
937
|
+
"- standard_optimization_no_writing_default: do not route into `write`, `review`, or `finalize` while this optimization task profile remains active unless the user explicitly broadens scope.",
|
|
938
|
+
"- standard_optimization_iteration_rule: prefer more justified optimization attempts, branch promotion, or frontier cleanup over paper-facing packaging.",
|
|
939
|
+
]
|
|
940
|
+
)
|
|
838
941
|
if need_research_paper:
|
|
839
942
|
lines.extend(
|
|
840
943
|
[
|
|
841
944
|
"- delivery_goal: the quest should normally continue until at least one paper-like deliverable exists.",
|
|
842
|
-
"- main_result_rule: a strong main experiment is evidence, not the endpoint; usually continue into
|
|
843
|
-
"-
|
|
844
|
-
"- main_run_branch_rule_2: if the current workspace is still an idea branch when `artifact.record_main_experiment(...)` runs, the runtime will materialize a child `run/*` branch before durable recording; still prefer planning and implementation with that dedicated run branch in mind from the start.",
|
|
845
|
-
"- paper_branch_rule: after the required analysis for a strong main result is complete, writing should continue on a dedicated `paper/*` branch/worktree derived from that run branch rather than on the quest root or on the evidence branch itself.",
|
|
846
|
-
"- paper_branch_rule_2: treat the paper branch as the writing surface and the parent run branch as the evidence source; do not record new main experiments from the paper branch.",
|
|
847
|
-
"- paper_template_rule: once paper writing starts, choose a real venue template from the `write` skill's `templates/` folder, copy it into `paper/latex/`, and default to `templates/iclr2026/` for general ML unless the user or venue contract clearly points elsewhere.",
|
|
848
|
-
"- writing_rule: when the evidence becomes strong enough, analysis and paper writing remain in scope by default.",
|
|
945
|
+
"- main_result_rule: a strong main experiment is evidence, not the endpoint; usually continue into analysis, writing, or strengthening work.",
|
|
946
|
+
"- paper_branch_rule: writing should normally continue on a dedicated `paper/*` branch/worktree derived from the evidence line rather than mutating the evidence branch itself.",
|
|
849
947
|
"- review_gate_rule: before declaring a substantial paper/draft task done, open `review` for an independent skeptical audit; if that audit finds serious gaps, route to `analysis-campaign`, `baseline`, `scout`, or `write` instead of stopping.",
|
|
850
948
|
"- stop_rule: do not stop with only an improved algorithm or isolated run logs unless the user explicitly narrows scope.",
|
|
851
949
|
]
|
|
@@ -854,6 +952,9 @@ class PromptBuilder:
|
|
|
854
952
|
lines.extend(
|
|
855
953
|
[
|
|
856
954
|
"- delivery_goal: the quest should pursue the strongest justified algorithmic result rather than paper packaging.",
|
|
955
|
+
"- optimization_object_rule: distinguish candidate briefs, durable optimization lines, and implementation-level optimization candidates; do not treat them as one object type.",
|
|
956
|
+
"- optimization_frontier_rule: before major route selection in algorithm-first work, read `artifact.get_optimization_frontier(...)` and treat the current frontier as the primary optimize-state summary.",
|
|
957
|
+
"- optimization_promotion_rule: `submission_mode='candidate'` is branchless pre-promotion state, while `submission_mode='line'` is a committed durable line with a branch/worktree.",
|
|
857
958
|
"- main_result_rule: use each measured main-experiment result to decide whether to create a `continue_line` child branch, create a `branch_alternative` sibling-like branch, run more analysis, or stop.",
|
|
858
959
|
"- no_paper_rule: do not default into `artifact.submit_paper_outline(...)`, `artifact.submit_paper_bundle(...)`, or `finalize` while this mode remains active.",
|
|
859
960
|
"- autonomy_rule: choose the next optimization foundation from durable evidence such as baseline state, the current research head, and recent main-experiment results; do not routinely ask the user to choose that.",
|
|
@@ -862,6 +963,69 @@ class PromptBuilder:
|
|
|
862
963
|
)
|
|
863
964
|
return "\n".join(lines)
|
|
864
965
|
|
|
966
|
+
def _optimization_frontier_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
967
|
+
active_anchor = str(snapshot.get("active_anchor") or "").strip().lower()
|
|
968
|
+
if self._need_research_paper(snapshot) and active_anchor != "optimize":
|
|
969
|
+
return "- not primary in the current delivery mode"
|
|
970
|
+
|
|
971
|
+
try:
|
|
972
|
+
from ..artifact import ArtifactService
|
|
973
|
+
|
|
974
|
+
payload = ArtifactService(self.home).get_optimization_frontier(quest_root)
|
|
975
|
+
except Exception:
|
|
976
|
+
payload = {"ok": False}
|
|
977
|
+
|
|
978
|
+
frontier = (
|
|
979
|
+
dict(payload.get("optimization_frontier") or {})
|
|
980
|
+
if isinstance(payload, dict) and isinstance(payload.get("optimization_frontier"), dict)
|
|
981
|
+
else {}
|
|
982
|
+
)
|
|
983
|
+
if not frontier:
|
|
984
|
+
return "- unavailable"
|
|
985
|
+
|
|
986
|
+
best_branch = dict(frontier.get("best_branch") or {}) if isinstance(frontier.get("best_branch"), dict) else {}
|
|
987
|
+
best_run = dict(frontier.get("best_run") or {}) if isinstance(frontier.get("best_run"), dict) else {}
|
|
988
|
+
backlog = dict(frontier.get("candidate_backlog") or {}) if isinstance(frontier.get("candidate_backlog"), dict) else {}
|
|
989
|
+
next_actions = [str(item).strip() for item in (frontier.get("recommended_next_actions") or []) if str(item).strip()]
|
|
990
|
+
stagnant = frontier.get("stagnant_branches") or []
|
|
991
|
+
fusion = frontier.get("fusion_candidates") or []
|
|
992
|
+
local_attempts = [
|
|
993
|
+
dict(item)
|
|
994
|
+
for item in (frontier.get("best_branch_recent_candidates") or [])
|
|
995
|
+
if isinstance(item, dict)
|
|
996
|
+
]
|
|
997
|
+
|
|
998
|
+
lines = [
|
|
999
|
+
f"- frontier_mode: {str(frontier.get('mode') or 'unknown')}",
|
|
1000
|
+
f"- frontier_reason: {str(frontier.get('frontier_reason') or 'none')}",
|
|
1001
|
+
f"- frontier_best_branch: {str(best_branch.get('branch_name') or best_branch.get('branch_no') or 'none')}",
|
|
1002
|
+
f"- frontier_best_run: {str(best_run.get('run_id') or 'none')}",
|
|
1003
|
+
f"- frontier_candidate_briefs: {int(backlog.get('candidate_brief_count') or 0)}",
|
|
1004
|
+
f"- frontier_active_implementation_candidates: {int(backlog.get('active_implementation_candidate_count') or 0)}",
|
|
1005
|
+
f"- frontier_failed_implementation_candidates: {int(backlog.get('failed_implementation_candidate_count') or 0)}",
|
|
1006
|
+
f"- frontier_stagnant_branch_count: {len([item for item in stagnant if isinstance(item, dict)])}",
|
|
1007
|
+
f"- frontier_fusion_candidate_count: {len([item for item in fusion if isinstance(item, dict)])}",
|
|
1008
|
+
"- optimization_frontier_rule: in algorithm-first work, treat this block as the primary route-selection surface before relying on paper-facing state.",
|
|
1009
|
+
]
|
|
1010
|
+
if local_attempts:
|
|
1011
|
+
parts: list[str] = []
|
|
1012
|
+
for item in local_attempts[-3:]:
|
|
1013
|
+
summary_bits = [
|
|
1014
|
+
str(item.get("candidate_id") or "").strip() or "candidate",
|
|
1015
|
+
str(item.get("status") or "").strip() or "unknown",
|
|
1016
|
+
str(item.get("strategy") or "").strip() or None,
|
|
1017
|
+
str(item.get("mechanism_family") or "").strip() or None,
|
|
1018
|
+
str(item.get("failure_kind") or "").strip() or None,
|
|
1019
|
+
]
|
|
1020
|
+
parts.append(" / ".join(bit for bit in summary_bits if bit))
|
|
1021
|
+
lines.append(f"- frontier_same_line_local_attempt_memory: {' | '.join(parts)}")
|
|
1022
|
+
lines.append(
|
|
1023
|
+
"- optimization_local_memory_rule: before seed, loop, or debug work on the leading line, inspect this same-line local attempt memory so you do not repeat a near-duplicate change blindly."
|
|
1024
|
+
)
|
|
1025
|
+
if next_actions:
|
|
1026
|
+
lines.append(f"- frontier_next_actions: {' | '.join(next_actions[:3])}")
|
|
1027
|
+
return "\n".join(lines)
|
|
1028
|
+
|
|
865
1029
|
def _interaction_style_block(self, *, default_locale: str, user_message: str, snapshot: dict) -> str:
|
|
866
1030
|
normalized_locale = str(default_locale or "").lower()
|
|
867
1031
|
chinese_turn = normalized_locale.startswith("zh") or bool(re.search(r"[\u4e00-\u9fff]", user_message))
|
|
@@ -869,6 +1033,7 @@ class PromptBuilder:
|
|
|
869
1033
|
need_research_paper = self._need_research_paper(snapshot)
|
|
870
1034
|
decision_policy = self._decision_policy(snapshot)
|
|
871
1035
|
launch_mode = self._launch_mode(snapshot)
|
|
1036
|
+
standard_profile = self._standard_profile(snapshot)
|
|
872
1037
|
custom_profile = self._custom_profile(snapshot)
|
|
873
1038
|
lines = [
|
|
874
1039
|
f"- configured_default_locale: {default_locale}",
|
|
@@ -876,74 +1041,39 @@ class PromptBuilder:
|
|
|
876
1041
|
f"- bound_conversation_count: {len(bound_conversations)}",
|
|
877
1042
|
f"- decision_policy: {decision_policy}",
|
|
878
1043
|
f"- launch_mode: {launch_mode}",
|
|
1044
|
+
f"- standard_profile: {standard_profile if launch_mode == 'standard' else 'n/a'}",
|
|
879
1045
|
f"- custom_profile: {custom_profile if launch_mode == 'custom' else 'n/a'}",
|
|
880
1046
|
"- collaboration_mode: long-horizon, continuity-first, artifact-aware",
|
|
881
1047
|
"- response_pattern: say what changed -> say what it means -> say what happens next",
|
|
882
1048
|
"- interaction_protocol: first message may be plain conversation; after that, treat artifact.interact threads and mailbox polls as the main continuity spine across TUI, web, and connectors",
|
|
1049
|
+
"- shared_interaction_contract_precedence: use the shared interaction contract as the default user-facing cadence; the rules below add runtime-specific execution behavior instead of restating the same chat cadence",
|
|
883
1050
|
"- mailbox_protocol: artifact.interact(include_recent_inbound_messages=True) is the queued human-message mailbox; when it returns user text, treat that input as higher priority than background subtasks until it has been acknowledged",
|
|
884
1051
|
"- acknowledgment_protocol: after artifact.interact returns any human message, immediately send one substantive artifact.interact(...) follow-up; if the active connector runtime already emitted a transport-level receipt acknowledgement, do not send a redundant receipt-only message; if answerable, answer directly, otherwise state the short plan, nearest checkpoint, and that the current background subtask is paused",
|
|
885
|
-
"- progress_protocol: emit artifact.interact(kind='progress', reply_mode='threaded', ...) at real human-meaningful checkpoints; if no natural checkpoint appears during active user-relevant work, prefer a concise keepalive once work has crossed roughly 6 tool calls with a human-meaningful delta, and do not drift beyond roughly 12 tool calls or about 8 minutes without a user-visible update",
|
|
886
|
-
"- stage_kickoff_protocol: after entering any stage or companion skill, send one user-visible artifact.interact progress update within the first 3 tool calls of substantial work",
|
|
887
|
-
"- read_plan_keepalive_protocol: if work is still mostly reading, searching, comparison, or planning, do not wait too long for a 'big result'; send one concise user-visible checkpoint after about 5 consecutive tool calls if the user would otherwise see silence",
|
|
888
1052
|
"- subtask_boundary_protocol: send a user-visible update whenever the active subtask changes materially, especially across intake -> audit, audit -> experiment planning, experiment planning -> run launch, run result -> drafting, or drafting -> review/rebuttal",
|
|
889
1053
|
"- smoke_then_detach_protocol: for baseline reproduction, main experiments, and analysis experiments, first validate the command path with a bounded smoke test; once the smoke test passes, launch the real long run with bash_exec(mode='detach', ...) and usually leave timeout_seconds unset rather than guessing a fake deadline",
|
|
890
1054
|
"- progress_first_monitoring_protocol: when supervising a long-running bash_exec session, judge health by forward progress rather than by whether the final artifact has already appeared within a short window",
|
|
891
|
-
"-
|
|
892
|
-
"- long_run_reporting_protocol: for long-running bash_exec monitoring loops, inspect real logs or status after each completed sleep/await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta or when the 30-minute visibility bound would otherwise be exceeded; those updates should report the current status, the latest concrete evidence of progress or failure, and the next checkpoint",
|
|
893
|
-
"- long_run_watchdog_protocol: for baseline reproduction, baseline-running stages, main experiments, and other important detached runs, do not let more than 30 minutes pass without a real progress inspection and, if the run is still active, a user-visible artifact.interact progress update",
|
|
1055
|
+
"- long_run_reporting_protocol: inspect real logs/status after each meaningful await cycle and at least once every 30 minutes at worst, but only send a user-visible update when there is a human-meaningful delta, blocker, recovery, route change, or the visibility bound would otherwise be exceeded",
|
|
894
1056
|
"- intervention_threshold_protocol: do not kill or restart a run merely because a short watch window passed without final completion; intervene only on explicit failure, clear invalidity, process exit, or no meaningful delta across a sufficiently long observation window",
|
|
895
|
-
"-
|
|
896
|
-
"- saved_log_read_protocol: bash_exec(mode='read', id=...) returns the full saved rendered log when it is 2000 lines or fewer; for longer logs it returns a preview with the first 500 lines plus the last 1500 lines and tells you to use start/tail for omitted middle windows",
|
|
897
|
-
"- log_window_protocol: when you need a specific omitted middle region from a long saved log, use bash_exec(mode='read', id=..., start=..., tail=...) to read a forward rendered-line window",
|
|
898
|
-
"- tail_monitoring_protocol: when monitoring a detached run, prefer bash_exec(mode='read', id=..., tail_limit=..., order='desc') so you inspect the newest seq-based evidence first instead of re-reading full logs every time",
|
|
899
|
-
"- managed_recovery_protocol: if a detached baseline, main-experiment, or analysis run is clearly invalid, wedged, or superseded, stop it with bash_exec(mode='kill', id=...), document the reason, fix the issue, and relaunch cleanly instead of letting a bad run linger",
|
|
900
|
-
"- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor, or set timeout_seconds intentionally",
|
|
1057
|
+
"- timeout_protocol: before using bash_exec(mode='await', ...), estimate whether the command can finish within the selected wait window; if runtime is uncertain or likely longer, use bash_exec(mode='detach', ...) and monitor instead of guessing a fake deadline",
|
|
901
1058
|
"- blocking_protocol: use reply_mode='blocking' only for true unresolved user decisions; ordinary progress updates should stay threaded and non-blocking",
|
|
902
1059
|
"- credential_blocking_protocol: if continuation requires user-supplied external credentials or secrets such as an API key, GitHub key/token, or Hugging Face key/token, emit one structured blocking decision request that asks the user to provide the credential or choose an alternative route; do not invent placeholders or silently skip the blocked step",
|
|
903
1060
|
"- credential_wait_protocol: if that credential request remains unanswered, keep the quest waiting rather than self-resolving; if you are resumed without new credentials and no other work is possible, a long low-frequency park such as `bash_exec(command='sleep 3600', mode='await', timeout_seconds=3700)` is acceptable to avoid busy-looping",
|
|
904
1061
|
f"- standby_prefix_rule: when you intentionally leave one blocking standby interaction after task completion, prefix it with {'[等待决策]' if chinese_turn else '[Waiting for decision]'} and wait for a new user reply before continuing",
|
|
905
1062
|
"- stop_notice_protocol: if work must pause or stop, send a user-visible notice that explains why, confirms preserved context, and states that any new message or `/resume` will continue from the same quest",
|
|
906
1063
|
"- respect_protocol: write user-facing updates as natural, respectful, easy-to-follow chat; do not sound like a formal status report or internal tool log",
|
|
907
|
-
"-
|
|
1064
|
+
"- novice_context_protocol: assume the user may not know the repo layout, branch model, artifact schema, or tool names; explain progress in task language first.",
|
|
1065
|
+
"- structure_protocol: when explaining 2 to 3 options, tradeoffs, or next steps, prefer a short numbered structure so the user can scan the decision surface quickly.",
|
|
1066
|
+
"- example_and_numbers_protocol: when it materially improves understanding, include one short example or 1 to 3 key numbers or comparisons instead of relying only on vague adjectives such as better, slower, or more stable.",
|
|
1067
|
+
"- omission_protocol: for ordinary user-facing updates, omit file paths, file names, artifact ids, branch/worktree ids, session ids, raw commands, raw logs, and internal tool names unless the user asked for them or needs them to act",
|
|
908
1068
|
"- compaction_protocol: ordinary artifact.interact progress updates should usually fit in 2 to 4 short sentences and should not read like a monitoring transcript or execution diary",
|
|
909
|
-
"-
|
|
1069
|
+
"- watchdog_payload_protocol: if a tool result includes `watchdog_notes`, `progress_watchdog_note`, `visibility_watchdog_note`, or `state_change_watchdog_note`, treat that as an action item to inspect state and decide whether a fresh user-visible update is actually needed; do not emit duplicate progress by reflex",
|
|
910
1070
|
"- human_progress_shape_protocol: ordinary progress updates should usually make three things explicit in human language: the current task, the main difficulty or latest real progress, and the concrete next measure you will take",
|
|
911
|
-
"-
|
|
912
|
-
"- eta_visibility_protocol: for baseline reproduction, main experiments, analysis experiments, and other important long-running phases, progress updates should also make the expected time to the next meaningful result, next milestone, or next user-visible update explicit; use roughly 10 to 30 minutes as the normal update window, and if the ETA is unreliable, say that and give a realistic next check-in window instead",
|
|
913
|
-
"- stage_plan_protocol: for `baseline`, `experiment`, and `analysis-campaign`, do not jump straight into substantial setup, code changes, or real runs; first create or update quest-visible `PLAN.md` and `CHECKLIST.md`, then keep them aligned with the actual route",
|
|
914
|
-
"- baseline_plan_protocol: in `baseline`, read the source paper and source repo first when they exist, then make `PLAN.md` cover the route, source package, code touchpoints, smoke path, real-run path, fallback options, monitoring rules, and verification targets before substantial work continues",
|
|
915
|
-
"- experiment_plan_protocol: in `experiment`, make `PLAN.md` start with the selected idea summarized in 1 to 2 sentences and then map the idea into code touchpoints, comparability rules, smoke / pilot path, full-run path, fallback options, monitoring rules, and revision notes",
|
|
916
|
-
"- analysis_plan_protocol: in `analysis-campaign`, treat `PLAN.md` as the campaign charter and make it cover the slice list, comparability boundary, asset and comparator plan, smoke / full-run policy, reporting plan, and revision log before real slices launch",
|
|
917
|
-
"- checklist_maintenance_protocol: for those same stages, treat `CHECKLIST.md` as the living execution surface and update it during reading, setup, coding, smoke tests, real runs, validation, aggregation, and route changes instead of letting progress live only in chat",
|
|
918
|
-
"- plan_revision_protocol: if the route, comparability contract, source package, execution strategy, slice ordering, or campaign interpretation changes materially, revise `PLAN.md` before continuing",
|
|
919
|
-
"- plan_execution_stability_protocol: once `baseline` or `experiment` has a concrete `PLAN.md` route, implement that plan cleanly instead of repeatedly reshaping code and commands mid-flight; the normal default is one bounded smoke or pilot validation and then one real run, and extra retries should happen only after concrete failure, invalidity, or genuinely new evidence justifies them",
|
|
920
|
-
"- stage_milestone_summary_protocol: for accepted baseline, selected idea, completed main experiment, and completed analysis-campaign milestones, usually open with 1 to 2 sentences that say what happened, what it means, and the exact next step before expanding into more detail",
|
|
921
|
-
"- idea_milestone_protocol: immediately after a successful accepted artifact.submit_idea(...), send a threaded milestone that explains the idea in plain language and explicitly states whether it currently looks valid, research-worthy, and insight-bearing, plus the main risk and exact next experiment",
|
|
922
|
-
"- idea_divergence_protocol: in the idea stage, separate divergence from convergence; unless strong durable evidence already narrows the route to one obvious serious option, do not collapse onto the first plausible route before generating a small but meaningfully diverse candidate slate",
|
|
923
|
-
"- idea_lens_protocol: when idea candidates cluster around one mechanism family, deliberately switch ideation lenses such as problem-first vs solution-first, tension hunting, analogy transfer, inversion, or adjacent-possible reasoning before final selection",
|
|
924
|
-
"- idea_frontier_protocol: a temporary raw ideation slate may be larger, but after convergence the serious frontier should usually shrink back to 2 to 3 candidates and at most 5",
|
|
925
|
-
"- idea_why_now_protocol: every serious idea candidate should answer why now or what changed, not just what the mechanism is",
|
|
926
|
-
"- idea_balance_protocol: when the search space is not tiny, carry at least one conservative route and one higher-upside route into the final comparison",
|
|
927
|
-
"- idea_pitch_protocol: before artifact.submit_idea(...), make the winner pass a two-sentence pitch, a strongest-objection check, and a concrete why-now statement",
|
|
928
|
-
"- idea_literature_floor_protocol: do not write or submit the final selected idea until the durable survey covers at least 5 and usually 5 to 10 related and usable papers; if fewer than 5 direct papers exist, document the shortage and use the closest adjacent translatable work instead of skipping the gate",
|
|
929
|
-
"- idea_reference_protocol: the final selected-idea draft should cite the survey-stage papers it actually uses and end with a standard-format `References` or `Bibliography` section",
|
|
930
|
-
"- experiment_milestone_protocol: immediately after artifact.record_main_experiment(...) writes the durable result, send a threaded milestone that explains what was run, the main result, whether primary performance improved / worsened / stayed mixed versus the active baseline or best prior anchor, whether the route still looks promising, and the exact next step",
|
|
931
|
-
"- analysis_milestone_protocol: immediately after a meaningful completed analysis-campaign synthesis or route-significant campaign checkpoint, send a threaded milestone that explains which campaign question or slice set just closed, whether the claim boundary became stronger / weaker / mixed, the main caveat, and the exact next route",
|
|
932
|
-
"- paper_milestone_protocol: immediately after a meaningful paper or draft milestone such as selected outline, evidence-complete draft, major revision package, or bundle-ready paper, send a threaded milestone that explains what document milestone is now complete, which claims are now supportable, what still needs strengthening, and the exact next revision or execution route",
|
|
933
|
-
"- asset_grounded_analysis_protocol: before artifact.create_analysis_campaign(...), reuse current quest and user-provided assets first and only plan slices that are executable with the current assets, runtime/tooling, and available credentials",
|
|
934
|
-
"- infeasible_slice_protocol: if an analysis slice cannot actually be executed after bounded recovery, do not fake completion; record the slice with a non-success status, report the blocker explicitly, and do not pretend the system can do it",
|
|
935
|
-
"- explicit_improvement_protocol: never make the user infer performance improvement only from raw metrics; say plainly whether performance improved, worsened, or stayed mixed",
|
|
936
|
-
"- verified_reference_breadth_protocol: for paper-like writing, run broad literature search and reading, aim for roughly 30 to 50 verified references unless scope clearly justifies fewer, use one consistent citation workflow SEARCH -> VERIFY -> RETRIEVE -> VALIDATE -> ADD, use Semantic Scholar by default or Google Scholar manual search/export for discovery, use DOI/Crossref or other real metadata backfills for BibTeX and verification, Every final citation must correspond to a real paper from an actual source, store actual bibliography entries in paper/references.bib as valid BibTeX, do one explicit reference audit before bundling, and never invent citations from memory or hand-write BibTeX from scratch",
|
|
937
|
-
"- narrative_focus_protocol: for paper-like writing, organize the paper around one cohesive contribution, make What / Why / So What clear early, assume many readers judge in the order title -> abstract -> introduction -> figures, front-load value in those surfaces, use a five-part abstract formula, keep the introduction concise with 2 to 4 specific contribution bullets, and if the first sentence could be pasted into many unrelated ML papers then rewrite it until it becomes specific",
|
|
938
|
-
"- writing_reasoning_externalization_protocol: for paper-like writing, externalize major reasoning into durable notes such as paper/outline_selection.md, paper/claim_evidence_map.json, paper/related_work_map.md, paper/figure_storyboard.md, and paper/reviewer_first_pass.md; those notes should summarize current judgment, alternatives considered, evidence used, risks, and next revision action rather than hidden chain-of-thought",
|
|
939
|
-
"- outline_intro_value_protocol: for outlines and introductions, make research value explicit early and use a standard introduction arc: problem and stakes -> concrete gap/bottleneck -> remedy/core idea -> evidence preview -> contributions",
|
|
1071
|
+
"- stage_contract_protocol: stage-specific plan/checklist rules, milestone rules, literature rules, and writing rules belong in the requested skill; do not expect this runtime block to restate them",
|
|
940
1072
|
"- teammate_voice_protocol: write like a calm capable teammate using natural first-person phrasing when helpful, for example 'I'm working on ...', 'The main issue right now is ...', 'Next I'll ...'; do not sound like a dashboard or incident log",
|
|
941
|
-
"-
|
|
942
|
-
"- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, counter, timestamp, or subprocess",
|
|
1073
|
+
"- translation_protocol: convert internal actions into user-facing meaning; describe what was finished and why it matters instead of naming every touched file, path, branch, counter, timestamp, or subprocess",
|
|
943
1074
|
"- detail_gate_protocol: include exact counters, worker labels, timestamps, retry counts, or file names only when the user explicitly asked for them, when they change the recommended action, or when they are the only honest way to explain a real blocker",
|
|
944
|
-
"- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window
|
|
1075
|
+
"- monitoring_summary_protocol: for long-running monitoring loops, summarize the frontier state in plain language such as still progressing, temporarily stalled, recovered, or needs intervention; do not narrate each watch window",
|
|
945
1076
|
"- preflight_rewrite_protocol: before sending artifact.interact, quickly self-check whether the draft reads like a monitoring log, file inventory, or internal diary; if it mentions watch windows, heartbeats, retry counters, raw counts, timestamps, or multiple file names without being necessary for user action, rewrite it into conclusion -> meaning -> next step first",
|
|
946
|
-
"- non_research_mode_protocol: if the user message looks like a non-research request, ask for a second confirmation before engaging stage skills or research workflow; after completion, leave one blocking standby interaction instead of repeatedly pinging",
|
|
947
1077
|
"- workspace_discipline: read and modify code inside current_workspace_root; treat quest_root as the canonical repo identity and durable runtime root",
|
|
948
1078
|
"- binary_safety: do not open or rewrite large binary assets unless truly necessary; prefer summaries, metadata, and targeted inspection first",
|
|
949
1079
|
]
|
|
@@ -958,7 +1088,7 @@ class PromptBuilder:
|
|
|
958
1088
|
else:
|
|
959
1089
|
lines.extend(
|
|
960
1090
|
[
|
|
961
|
-
"- user_gated_decision_protocol: when continuation truly depends on user preference, approval, or scope choice, use one structured blocking decision request with 1 to 3 concrete options.",
|
|
1091
|
+
"- user_gated_decision_protocol: when continuation truly depends on user preference, approval, or scope choice, use one structured blocking decision request with 1 to 3 concrete options; for each option say what it means, how strongly you recommend it, and what impact it would have on speed, quality, cost, or risk.",
|
|
962
1092
|
"- user_gated_restraint: even in user-gated mode, do not turn ordinary progress or ordinary stage completion into blocking interrupts.",
|
|
963
1093
|
]
|
|
964
1094
|
)
|
|
@@ -970,6 +1100,10 @@ class PromptBuilder:
|
|
|
970
1100
|
lines.append(
|
|
971
1101
|
"- completion_protocol: when `startup_contract.need_research_paper` is false, the quest goal is the strongest justified algorithmic result; keep iterating from measured main-experiment results and do not self-route into paper work by default"
|
|
972
1102
|
)
|
|
1103
|
+
if launch_mode == "standard" and standard_profile == "optimization_task":
|
|
1104
|
+
lines.append(
|
|
1105
|
+
"- standard_optimization_completion_protocol: in this entry profile, do not treat missing paper artifacts or missing analysis-campaign artifacts as unfinished work by themselves; keep pushing the optimization frontier until the result plateaus, a blocker appears, or the user changes scope."
|
|
1106
|
+
)
|
|
973
1107
|
if chinese_turn:
|
|
974
1108
|
lines.extend(
|
|
975
1109
|
[
|
|
@@ -987,137 +1121,40 @@ class PromptBuilder:
|
|
|
987
1121
|
return "\n".join(lines)
|
|
988
1122
|
|
|
989
1123
|
def _quest_context_block(self, quest_root: Path) -> str:
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
)
|
|
997
|
-
text = read_text(quest_root / filename).strip() or "(empty)"
|
|
998
|
-
parts.extend([f"{title} ({filename}):", text, ""])
|
|
999
|
-
return "\n".join(parts).strip()
|
|
1124
|
+
return "\n".join(
|
|
1125
|
+
[
|
|
1126
|
+
"- quest_context_rule: quest documents are durable but not pre-expanded here.",
|
|
1127
|
+
"- quest_documents_tool: call artifact.read_quest_documents(names=['brief','plan','status','summary'], mode='excerpt'|'full') when document detail is needed.",
|
|
1128
|
+
"- active_user_requirements_tool: call artifact.read_quest_documents(names=['active_user_requirements'], mode='full') when exact current durable user requirements matter.",
|
|
1129
|
+
]
|
|
1130
|
+
)
|
|
1000
1131
|
|
|
1001
1132
|
def _durable_state_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
1002
|
-
requested_baseline_ref = (
|
|
1003
|
-
dict(snapshot.get("requested_baseline_ref") or {})
|
|
1004
|
-
if isinstance(snapshot.get("requested_baseline_ref"), dict)
|
|
1005
|
-
else None
|
|
1006
|
-
)
|
|
1007
|
-
startup_contract = (
|
|
1008
|
-
dict(snapshot.get("startup_contract") or {})
|
|
1009
|
-
if isinstance(snapshot.get("startup_contract"), dict)
|
|
1010
|
-
else None
|
|
1011
|
-
)
|
|
1012
1133
|
confirmed_baseline_ref = (
|
|
1013
1134
|
dict(snapshot.get("confirmed_baseline_ref") or {})
|
|
1014
1135
|
if isinstance(snapshot.get("confirmed_baseline_ref"), dict)
|
|
1015
|
-
else
|
|
1136
|
+
else {}
|
|
1016
1137
|
)
|
|
1017
|
-
requested_baseline_id = str((requested_baseline_ref or {}).get("baseline_id") or "").strip()
|
|
1018
|
-
confirmed_baseline_id = str((confirmed_baseline_ref or {}).get("baseline_id") or "").strip()
|
|
1019
|
-
confirmed_baseline_rel_path = str(
|
|
1020
|
-
(confirmed_baseline_ref or {}).get("baseline_root_rel_path") or ""
|
|
1021
|
-
).strip()
|
|
1022
1138
|
confirmed_metric_contract_json_rel_path = str(
|
|
1023
|
-
|
|
1139
|
+
confirmed_baseline_ref.get("metric_contract_json_rel_path") or ""
|
|
1024
1140
|
).strip()
|
|
1025
|
-
prebound_baseline_ready = bool(
|
|
1026
|
-
requested_baseline_id
|
|
1027
|
-
and confirmed_baseline_id
|
|
1028
|
-
and requested_baseline_id == confirmed_baseline_id
|
|
1029
|
-
and str(snapshot.get("baseline_gate") or "").strip().lower() == "confirmed"
|
|
1030
|
-
)
|
|
1031
1141
|
lines = [
|
|
1032
1142
|
f"- baseline_gate: {snapshot.get('baseline_gate') or 'pending'}",
|
|
1033
1143
|
f"- active_baseline_id: {snapshot.get('active_baseline_id') or 'none'}",
|
|
1034
|
-
f"- active_baseline_variant_id: {snapshot.get('active_baseline_variant_id') or 'none'}",
|
|
1035
|
-
f"- requested_baseline_ref: {json.dumps(requested_baseline_ref, ensure_ascii=False, sort_keys=True) if requested_baseline_ref else 'none'}",
|
|
1036
|
-
f"- startup_contract: {json.dumps(startup_contract, ensure_ascii=False, sort_keys=True) if startup_contract else 'none'}",
|
|
1037
|
-
f"- startup_decision_policy: {self._decision_policy(snapshot)}",
|
|
1038
|
-
f"- confirmed_baseline_ref: {json.dumps(confirmed_baseline_ref, ensure_ascii=False, sort_keys=True) if confirmed_baseline_ref else 'none'}",
|
|
1039
|
-
f"- confirmed_baseline_import_root: {confirmed_baseline_rel_path or 'none'}",
|
|
1040
|
-
f"- prebound_baseline_ready: {prebound_baseline_ready}",
|
|
1041
1144
|
f"- active_run_id: {snapshot.get('active_run_id') or 'none'}",
|
|
1042
|
-
f"- research_head_branch: {snapshot.get('research_head_branch') or 'none'}",
|
|
1043
|
-
f"- research_head_worktree_root: {snapshot.get('research_head_worktree_root') or 'none'}",
|
|
1044
|
-
f"- current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
|
|
1045
|
-
f"- current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
|
|
1046
1145
|
f"- active_idea_id: {snapshot.get('active_idea_id') or 'none'}",
|
|
1047
|
-
f"- active_idea_md_path: {snapshot.get('active_idea_md_path') or 'none'}",
|
|
1048
1146
|
f"- active_analysis_campaign_id: {snapshot.get('active_analysis_campaign_id') or 'none'}",
|
|
1049
|
-
f"-
|
|
1147
|
+
f"- active_paper_line_ref: {snapshot.get('active_paper_line_ref') or 'none'}",
|
|
1148
|
+
f"- current_workspace_branch: {snapshot.get('current_workspace_branch') or 'none'}",
|
|
1149
|
+
f"- current_workspace_root: {snapshot.get('current_workspace_root') or 'none'}",
|
|
1050
1150
|
f"- workspace_mode: {snapshot.get('workspace_mode') or 'quest'}",
|
|
1051
1151
|
f"- runtime_status: {snapshot.get('runtime_status') or snapshot.get('status') or 'unknown'}",
|
|
1052
|
-
f"- stop_reason: {snapshot.get('stop_reason') or 'none'}",
|
|
1053
|
-
f"- pending_decisions: {', '.join(snapshot.get('pending_decisions') or []) or 'none'}",
|
|
1054
|
-
f"- pending_user_message_count: {snapshot.get('pending_user_message_count') or 0}",
|
|
1055
|
-
f"- active_interaction_count: {len(snapshot.get('active_interactions') or [])}",
|
|
1056
1152
|
f"- waiting_interaction_id: {snapshot.get('waiting_interaction_id') or 'none'}",
|
|
1057
|
-
f"-
|
|
1058
|
-
f"-
|
|
1059
|
-
f"-
|
|
1060
|
-
|
|
1061
|
-
f"- bound_conversations: {', '.join(snapshot.get('bound_conversations') or []) or 'none'}",
|
|
1062
|
-
f"- cloud_linked: {snapshot.get('cloud', {}).get('linked', False)}",
|
|
1153
|
+
f"- pending_user_message_count: {snapshot.get('pending_user_message_count') or 0}",
|
|
1154
|
+
f"- continuation_policy: {snapshot.get('continuation_policy') or 'auto'}",
|
|
1155
|
+
f"- continuation_anchor: {snapshot.get('continuation_anchor') or 'none'}",
|
|
1156
|
+
"- quest_state_tool: call artifact.get_quest_state(detail='summary'|'full') for current runtime refs, interactions, recent artifacts, and recent runs.",
|
|
1063
1157
|
]
|
|
1064
|
-
if prebound_baseline_ready and confirmed_baseline_rel_path:
|
|
1065
|
-
lines.extend(
|
|
1066
|
-
[
|
|
1067
|
-
"- prebound_baseline_execution_policy: runtime already attached and confirmed the requested baseline before this turn.",
|
|
1068
|
-
f"- prebound_baseline_runtime_path: {confirmed_baseline_rel_path}",
|
|
1069
|
-
"- prebound_baseline_agent_rule: do not redo baseline discovery or reproduction unless you find a concrete incompatibility, corruption, or missing evidence problem.",
|
|
1070
|
-
]
|
|
1071
|
-
)
|
|
1072
|
-
active_workspace_root = Path(str(snapshot.get("current_workspace_root") or quest_root))
|
|
1073
|
-
attachment_root = active_workspace_root / "baselines" / "imported"
|
|
1074
|
-
if attachment_root.exists():
|
|
1075
|
-
attachments = [read_yaml(path, {}) for path in sorted(attachment_root.glob("*/attachment.yaml"))]
|
|
1076
|
-
attachments = [
|
|
1077
|
-
item
|
|
1078
|
-
for item in attachments
|
|
1079
|
-
if isinstance(item, dict)
|
|
1080
|
-
and item
|
|
1081
|
-
and (
|
|
1082
|
-
not str(item.get("source_baseline_id") or "").strip()
|
|
1083
|
-
or not self.baseline_registry.is_deleted(str(item.get("source_baseline_id") or "").strip())
|
|
1084
|
-
)
|
|
1085
|
-
]
|
|
1086
|
-
if attachments:
|
|
1087
|
-
attachment = max(
|
|
1088
|
-
attachments,
|
|
1089
|
-
key=lambda item: (
|
|
1090
|
-
str(item.get("attached_at") or ""),
|
|
1091
|
-
str(item.get("source_baseline_id") or ""),
|
|
1092
|
-
),
|
|
1093
|
-
)
|
|
1094
|
-
entry = attachment.get("entry") if isinstance(attachment.get("entry"), dict) else {}
|
|
1095
|
-
confirmation = attachment.get("confirmation") if isinstance(attachment.get("confirmation"), dict) else {}
|
|
1096
|
-
if not confirmed_metric_contract_json_rel_path:
|
|
1097
|
-
confirmed_metric_contract_json_rel_path = str(
|
|
1098
|
-
confirmation.get("metric_contract_json_rel_path") or ""
|
|
1099
|
-
).strip()
|
|
1100
|
-
contract = entry.get("metric_contract") if isinstance(entry.get("metric_contract"), dict) else {}
|
|
1101
|
-
primary_metric_id = str(contract.get("primary_metric_id") or "").strip() or "none"
|
|
1102
|
-
metric_ids = [
|
|
1103
|
-
str(item.get("metric_id") or "").strip()
|
|
1104
|
-
for item in contract.get("metrics", [])
|
|
1105
|
-
if isinstance(item, dict) and str(item.get("metric_id") or "").strip()
|
|
1106
|
-
]
|
|
1107
|
-
lines.extend(
|
|
1108
|
-
[
|
|
1109
|
-
f"- active_baseline_primary_metric_id: {primary_metric_id}",
|
|
1110
|
-
f"- active_baseline_metric_ids: {', '.join(metric_ids) or 'none'}",
|
|
1111
|
-
]
|
|
1112
|
-
)
|
|
1113
|
-
if (
|
|
1114
|
-
not confirmed_metric_contract_json_rel_path
|
|
1115
|
-
and confirmed_baseline_rel_path
|
|
1116
|
-
and (quest_root / confirmed_baseline_rel_path / "json" / "metric_contract.json").exists()
|
|
1117
|
-
):
|
|
1118
|
-
confirmed_metric_contract_json_rel_path = str(
|
|
1119
|
-
Path(confirmed_baseline_rel_path, "json", "metric_contract.json").as_posix()
|
|
1120
|
-
)
|
|
1121
1158
|
if confirmed_metric_contract_json_rel_path:
|
|
1122
1159
|
lines.extend(
|
|
1123
1160
|
[
|
|
@@ -1125,256 +1162,42 @@ class PromptBuilder:
|
|
|
1125
1162
|
"- active_baseline_metric_contract_rule: before planning or running `experiment` or `analysis-campaign`, read this JSON file and treat it as the canonical baseline comparison contract unless a newer confirmed baseline explicitly replaces it.",
|
|
1126
1163
|
]
|
|
1127
1164
|
)
|
|
1128
|
-
analysis_baseline_inventory = read_json(quest_root / "artifacts" / "baselines" / "analysis_inventory.json", {})
|
|
1129
|
-
analysis_baseline_inventory = analysis_baseline_inventory if isinstance(analysis_baseline_inventory, dict) else {}
|
|
1130
|
-
analysis_inventory_entries = (
|
|
1131
|
-
analysis_baseline_inventory.get("entries") if isinstance(analysis_baseline_inventory.get("entries"), list) else []
|
|
1132
|
-
)
|
|
1133
|
-
registered_count = sum(
|
|
1134
|
-
1
|
|
1135
|
-
for item in analysis_inventory_entries
|
|
1136
|
-
if isinstance(item, dict) and str(item.get("status") or "").strip().lower() == "registered"
|
|
1137
|
-
)
|
|
1138
|
-
if analysis_inventory_entries:
|
|
1139
|
-
lines.extend(
|
|
1140
|
-
[
|
|
1141
|
-
f"- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [exists]",
|
|
1142
|
-
f"- supplementary_baseline_count: {len(analysis_inventory_entries)}",
|
|
1143
|
-
f"- supplementary_baseline_registered_count: {registered_count}",
|
|
1144
|
-
]
|
|
1145
|
-
)
|
|
1146
|
-
else:
|
|
1147
|
-
lines.append("- supplementary_baseline_inventory_status: artifacts/baselines/analysis_inventory.json [missing]")
|
|
1148
|
-
lines.extend(["", "Active interactions:"])
|
|
1149
|
-
active_interactions = snapshot.get("active_interactions") or []
|
|
1150
|
-
if active_interactions:
|
|
1151
|
-
for item in active_interactions[-3:]:
|
|
1152
|
-
interaction_id = item.get("interaction_id") or item.get("artifact_id") or "interaction"
|
|
1153
|
-
status = item.get("status") or "unknown"
|
|
1154
|
-
message = str(item.get("message") or "").strip().replace("\n", " ")
|
|
1155
|
-
if len(message) > 180:
|
|
1156
|
-
message = message[:177].rstrip() + "..."
|
|
1157
|
-
lines.append(f"- {interaction_id} [{status}] {message or '(no message)'}")
|
|
1158
|
-
else:
|
|
1159
|
-
lines.append("- none")
|
|
1160
|
-
if int(snapshot.get("pending_user_message_count") or 0) > 0:
|
|
1161
|
-
lines.extend(
|
|
1162
|
-
[
|
|
1163
|
-
"",
|
|
1164
|
-
"Queued user-message notice:",
|
|
1165
|
-
"- There are queued user messages waiting to be picked up via artifact.interact(include_recent_inbound_messages=True).",
|
|
1166
|
-
"- Before continuing a resumed or follow-up turn, retrieve that mailbox payload first.",
|
|
1167
|
-
"- After the mailbox returns user text, immediately send a follow-up artifact.interact acknowledgement or direct answer before resuming background work.",
|
|
1168
|
-
]
|
|
1169
|
-
)
|
|
1170
|
-
|
|
1171
|
-
lines.extend(
|
|
1172
|
-
[
|
|
1173
|
-
"",
|
|
1174
|
-
"Recent artifacts:",
|
|
1175
|
-
]
|
|
1176
|
-
)
|
|
1177
|
-
recent_artifacts = snapshot.get("recent_artifacts") or []
|
|
1178
|
-
if recent_artifacts:
|
|
1179
|
-
for item in recent_artifacts[-5:]:
|
|
1180
|
-
payload = item.get("payload") or {}
|
|
1181
|
-
label = payload.get("artifact_id") or Path(item.get("path", "")).stem or "artifact"
|
|
1182
|
-
summary = payload.get("summary") or payload.get("reason") or "No summary provided."
|
|
1183
|
-
lines.append(f"- {item.get('kind')}: {label} -> {summary}")
|
|
1184
|
-
else:
|
|
1185
|
-
lines.append("- none")
|
|
1186
|
-
|
|
1187
|
-
lines.extend(["", "Recent runs:"])
|
|
1188
|
-
recent_runs = snapshot.get("recent_runs") or []
|
|
1189
|
-
if recent_runs:
|
|
1190
|
-
for item in recent_runs[-5:]:
|
|
1191
|
-
run_id = item.get("run_id") or "unknown-run"
|
|
1192
|
-
summary = item.get("summary") or "No summary provided."
|
|
1193
|
-
lines.append(f"- {run_id}: {summary}")
|
|
1194
|
-
else:
|
|
1195
|
-
lines.append("- none")
|
|
1196
|
-
|
|
1197
|
-
lines.extend(["", "Recent quest memory cards:"])
|
|
1198
|
-
quest_cards = self.memory_service.list_recent(scope="quest", quest_root=quest_root, limit=5)
|
|
1199
|
-
if quest_cards:
|
|
1200
|
-
for card in quest_cards:
|
|
1201
|
-
lines.append(f"- {card.get('type')}: {card.get('title')} ({card.get('path')})")
|
|
1202
|
-
else:
|
|
1203
|
-
lines.append("- none")
|
|
1204
|
-
|
|
1205
|
-
lines.extend(["", "Recent global memory cards:"])
|
|
1206
|
-
global_cards = self.memory_service.list_recent(scope="global", limit=3)
|
|
1207
|
-
if global_cards:
|
|
1208
|
-
for card in global_cards:
|
|
1209
|
-
lines.append(f"- {card.get('type')}: {card.get('title')} ({card.get('path')})")
|
|
1210
|
-
else:
|
|
1211
|
-
lines.append("- none")
|
|
1212
|
-
|
|
1213
|
-
lines.extend(["", "Reusable baselines:"])
|
|
1214
|
-
baseline_entries = self.baseline_registry.list_entries()[-5:]
|
|
1215
|
-
if baseline_entries:
|
|
1216
|
-
for entry in baseline_entries:
|
|
1217
|
-
baseline_id = entry.get("baseline_id") or entry.get("entry_id") or "unknown-baseline"
|
|
1218
|
-
summary = entry.get("summary") or entry.get("task") or "No summary provided."
|
|
1219
|
-
status = str(entry.get("status") or "unknown").strip() or "unknown"
|
|
1220
|
-
lines.append(f"- {baseline_id} [{status}]: {summary}")
|
|
1221
|
-
else:
|
|
1222
|
-
lines.append("- none")
|
|
1223
1165
|
return "\n".join(lines)
|
|
1224
1166
|
|
|
1225
1167
|
def _paper_and_evidence_block(self, snapshot: dict, quest_root: Path) -> str:
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
paper_root = quest_root / "paper"
|
|
1230
|
-
open_source_root = workspace_root / "release" / "open_source"
|
|
1231
|
-
if not open_source_root.exists():
|
|
1232
|
-
open_source_root = quest_root / "release" / "open_source"
|
|
1233
|
-
selected_outline = read_json(paper_root / "selected_outline.json", {})
|
|
1234
|
-
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1235
|
-
detailed_outline = (
|
|
1236
|
-
dict(selected_outline.get("detailed_outline") or {})
|
|
1237
|
-
if isinstance(selected_outline.get("detailed_outline"), dict)
|
|
1168
|
+
paper_contract = (
|
|
1169
|
+
dict(snapshot.get("paper_contract") or {})
|
|
1170
|
+
if isinstance(snapshot.get("paper_contract"), dict)
|
|
1238
1171
|
else {}
|
|
1239
1172
|
)
|
|
1240
|
-
bundle_manifest = read_json(paper_root / "paper_bundle_manifest.json", {})
|
|
1241
|
-
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
1242
|
-
paper_baseline_inventory = read_json(paper_root / "baseline_inventory.json", {})
|
|
1243
|
-
paper_baseline_inventory = paper_baseline_inventory if isinstance(paper_baseline_inventory, dict) else {}
|
|
1244
|
-
claim_evidence_map = read_json(paper_root / "claim_evidence_map.json", {})
|
|
1245
|
-
claim_evidence_map = claim_evidence_map if isinstance(claim_evidence_map, dict) else {}
|
|
1246
|
-
compile_report = read_json(paper_root / "build" / "compile_report.json", {})
|
|
1247
|
-
compile_report = compile_report if isinstance(compile_report, dict) else {}
|
|
1248
|
-
open_source_manifest = read_json(open_source_root / "manifest.json", {})
|
|
1249
|
-
open_source_manifest = open_source_manifest if isinstance(open_source_manifest, dict) else {}
|
|
1250
|
-
default_paper_prefix = (
|
|
1251
|
-
paper_root.relative_to(quest_root).as_posix()
|
|
1252
|
-
if paper_root.is_relative_to(quest_root)
|
|
1253
|
-
else "paper"
|
|
1254
|
-
)
|
|
1255
|
-
default_release_prefix = (
|
|
1256
|
-
open_source_root.relative_to(quest_root).as_posix()
|
|
1257
|
-
if open_source_root.is_relative_to(quest_root)
|
|
1258
|
-
else "release/open_source"
|
|
1259
|
-
)
|
|
1260
|
-
|
|
1261
|
-
selected_outline_ref = str(
|
|
1262
|
-
selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or ""
|
|
1263
|
-
).strip()
|
|
1264
|
-
selected_outline_title = str(
|
|
1265
|
-
detailed_outline.get("title") or selected_outline.get("title") or bundle_manifest.get("title") or ""
|
|
1266
|
-
).strip()
|
|
1267
|
-
research_questions_raw = detailed_outline.get("research_questions")
|
|
1268
|
-
research_questions: list[str] = []
|
|
1269
|
-
if isinstance(research_questions_raw, list):
|
|
1270
|
-
for item in research_questions_raw:
|
|
1271
|
-
if isinstance(item, dict):
|
|
1272
|
-
question = str(item.get("question_text") or item.get("title") or item.get("id") or "").strip()
|
|
1273
|
-
else:
|
|
1274
|
-
question = str(item or "").strip()
|
|
1275
|
-
if question:
|
|
1276
|
-
research_questions.append(question)
|
|
1277
|
-
|
|
1278
1173
|
lines = [
|
|
1279
|
-
f"- selected_outline_ref: {selected_outline_ref or 'none'}",
|
|
1280
|
-
f"- selected_outline_title: {
|
|
1281
|
-
f"- selected_outline_story_present: {bool(selected_outline.get('story'))}",
|
|
1282
|
-
f"- selected_outline_ten_questions_present: {bool(selected_outline.get('ten_questions'))}",
|
|
1283
|
-
f"- active_research_question_count: {len(research_questions)}",
|
|
1174
|
+
f"- selected_outline_ref: {str(paper_contract.get('selected_outline_ref') or 'none')}",
|
|
1175
|
+
f"- selected_outline_title: {str(paper_contract.get('title') or 'none')}",
|
|
1284
1176
|
]
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
def _path_status(path_str: str | None, *, fallback: str) -> str:
|
|
1290
|
-
resolved = str(path_str or fallback).strip() or fallback
|
|
1291
|
-
exists = (quest_root / resolved).exists()
|
|
1292
|
-
return f"{resolved} [{'exists' if exists else 'missing'}]"
|
|
1293
|
-
|
|
1294
|
-
lines.extend(
|
|
1295
|
-
[
|
|
1296
|
-
f"- writing_plan_status: {_path_status(bundle_manifest.get('writing_plan_path'), fallback=f'{default_paper_prefix}/writing_plan.md')}",
|
|
1297
|
-
f"- draft_status: {_path_status(bundle_manifest.get('draft_path'), fallback=f'{default_paper_prefix}/draft.md')}",
|
|
1298
|
-
f"- references_status: {_path_status(bundle_manifest.get('references_path'), fallback=f'{default_paper_prefix}/references.bib')}",
|
|
1299
|
-
f"- claim_evidence_map_status: {_path_status(bundle_manifest.get('claim_evidence_map_path'), fallback=f'{default_paper_prefix}/claim_evidence_map.json')}",
|
|
1300
|
-
f"- baseline_inventory_status: {_path_status(bundle_manifest.get('baseline_inventory_path'), fallback=f'{default_paper_prefix}/baseline_inventory.json')}",
|
|
1301
|
-
f"- review_status: {f'{default_paper_prefix}/review/review.md [exists]' if (paper_root / 'review' / 'review.md').exists() else f'{default_paper_prefix}/review/review.md [missing]'}",
|
|
1302
|
-
f"- proofing_report_status: {f'{default_paper_prefix}/proofing/proofing_report.md [exists]' if (paper_root / 'proofing' / 'proofing_report.md').exists() else f'{default_paper_prefix}/proofing/proofing_report.md [missing]'}",
|
|
1303
|
-
f"- page_images_manifest_status: {f'{default_paper_prefix}/proofing/page_images_manifest.json [exists]' if (paper_root / 'proofing' / 'page_images_manifest.json').exists() else f'{default_paper_prefix}/proofing/page_images_manifest.json [missing]'}",
|
|
1304
|
-
]
|
|
1177
|
+
paper_contract_health = (
|
|
1178
|
+
dict(snapshot.get("paper_contract_health") or {})
|
|
1179
|
+
if isinstance(snapshot.get("paper_contract_health"), dict)
|
|
1180
|
+
else {}
|
|
1305
1181
|
)
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
latex_root_path = str(bundle_manifest.get("latex_root_path") or "").strip()
|
|
1182
|
+
if paper_contract_health:
|
|
1183
|
+
primary_blocker = str(
|
|
1184
|
+
((paper_contract_health.get("blocking_reasons") or [None])[0]) or "none"
|
|
1185
|
+
).strip() or "none"
|
|
1311
1186
|
lines.extend(
|
|
1312
1187
|
[
|
|
1313
|
-
"-
|
|
1314
|
-
f"-
|
|
1315
|
-
f"-
|
|
1316
|
-
f"-
|
|
1317
|
-
f"-
|
|
1318
|
-
|
|
1188
|
+
f"- paper_contract_health: {'ready' if bool(paper_contract_health.get('writing_ready')) else 'blocked'}",
|
|
1189
|
+
f"- paper_health_counts: unresolved_required={int(paper_contract_health.get('unresolved_required_count') or 0)}, unmapped_completed={int(paper_contract_health.get('unmapped_completed_count') or 0)}, blocking_pending={int(paper_contract_health.get('blocking_open_supplementary_count') or 0)}",
|
|
1190
|
+
f"- paper_recommended_next_stage: {str(paper_contract_health.get('recommended_next_stage') or 'none')}",
|
|
1191
|
+
f"- paper_recommended_action: {str(paper_contract_health.get('recommended_action') or 'none')}",
|
|
1192
|
+
f"- paper_primary_blocker: {primary_blocker}",
|
|
1193
|
+
"- paper_health_tool: call artifact.get_paper_contract_health(detail='full') before paper-facing write/finalize work when the exact blocking items matter.",
|
|
1194
|
+
"- paper_outline_tool: call artifact.list_paper_outlines(...) when outline inventory or a valid outline_id is needed.",
|
|
1195
|
+
"- paper_campaign_tool: call artifact.get_analysis_campaign(campaign_id='active') when exact supplementary slice status matters.",
|
|
1319
1196
|
]
|
|
1320
1197
|
)
|
|
1321
|
-
else:
|
|
1322
|
-
lines.append("- paper_bundle_manifest_present: False")
|
|
1323
|
-
|
|
1324
|
-
claims = claim_evidence_map.get("claims") if isinstance(claim_evidence_map.get("claims"), list) else []
|
|
1325
|
-
counts = {"supported": 0, "partial": 0, "unsupported": 0, "deferred": 0}
|
|
1326
|
-
unresolved: list[str] = []
|
|
1327
|
-
for item in claims:
|
|
1328
|
-
if not isinstance(item, dict):
|
|
1329
|
-
continue
|
|
1330
|
-
status = str(item.get("support_status") or "").strip().lower()
|
|
1331
|
-
if status in counts:
|
|
1332
|
-
counts[status] += 1
|
|
1333
|
-
if status in {"partial", "unsupported", "deferred"}:
|
|
1334
|
-
claim_id = str(item.get("claim_id") or item.get("claim_text") or "claim").strip()
|
|
1335
|
-
unresolved.append(f"{claim_id} [{status}]")
|
|
1336
|
-
lines.append(
|
|
1337
|
-
"- claim_status_counts: "
|
|
1338
|
-
+ ", ".join(f"{key}={value}" for key, value in counts.items())
|
|
1339
|
-
)
|
|
1340
|
-
if unresolved:
|
|
1341
|
-
lines.append(f"- downgrade_watchlist: {'; '.join(unresolved[:5])}")
|
|
1342
|
-
else:
|
|
1343
|
-
lines.append("- downgrade_watchlist: none")
|
|
1344
|
-
|
|
1345
|
-
if compile_report:
|
|
1346
|
-
lines.append(f"- compile_report_ok: {compile_report.get('ok') if 'ok' in compile_report else 'unknown'}")
|
|
1347
|
-
supplementary_baselines = (
|
|
1348
|
-
paper_baseline_inventory.get("supplementary_baselines")
|
|
1349
|
-
if isinstance(paper_baseline_inventory.get("supplementary_baselines"), list)
|
|
1350
|
-
else []
|
|
1351
|
-
)
|
|
1352
|
-
if paper_baseline_inventory:
|
|
1353
|
-
lines.append(f"- paper_supplementary_baseline_count: {len(supplementary_baselines)}")
|
|
1354
|
-
if open_source_manifest:
|
|
1355
1198
|
lines.append(
|
|
1356
|
-
|
|
1199
|
+
"- paper_contract_rule: if the paper state is blocked, do not stabilize draft prose as if the paper were settled; follow the recommended paper action first."
|
|
1357
1200
|
)
|
|
1358
|
-
|
|
1359
|
-
lines.extend(["", "Recent supporting runs:"])
|
|
1360
|
-
recent_runs = snapshot.get("recent_runs") or []
|
|
1361
|
-
supporting_runs = [
|
|
1362
|
-
item
|
|
1363
|
-
for item in recent_runs
|
|
1364
|
-
if isinstance(item, dict) and str(item.get("run_id") or "").strip()
|
|
1365
|
-
]
|
|
1366
|
-
if supporting_runs:
|
|
1367
|
-
for item in supporting_runs[-3:]:
|
|
1368
|
-
run_id = str(item.get("run_id") or "run").strip()
|
|
1369
|
-
summary = str(item.get("summary") or "").strip() or "No summary provided."
|
|
1370
|
-
lines.append(f"- {run_id}: {summary}")
|
|
1371
|
-
else:
|
|
1372
|
-
lines.append("- none")
|
|
1373
|
-
|
|
1374
|
-
lines.append("")
|
|
1375
|
-
lines.append(
|
|
1376
|
-
"- paper_state_rule: when drafting, reviewing, bundling, or finalizing, treat the selected outline, claim-evidence map, bundle manifest, proofing outputs, and downgrade watchlist as the active writing truth surface."
|
|
1377
|
-
)
|
|
1378
1201
|
return "\n".join(lines)
|
|
1379
1202
|
|
|
1380
1203
|
def _priority_memory_block(
|
|
@@ -1387,46 +1210,15 @@ class PromptBuilder:
|
|
|
1387
1210
|
) -> str:
|
|
1388
1211
|
stage = active_anchor if active_anchor in STAGE_MEMORY_PLAN else skill_id
|
|
1389
1212
|
plan = STAGE_MEMORY_PLAN.get(stage, STAGE_MEMORY_PLAN["decision"])
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
limit=2,
|
|
1400
|
-
)
|
|
1401
|
-
if not cards:
|
|
1402
|
-
continue
|
|
1403
|
-
self._append_priority_memory(
|
|
1404
|
-
selected,
|
|
1405
|
-
seen_paths,
|
|
1406
|
-
card=cards[-1],
|
|
1407
|
-
scope=scope,
|
|
1408
|
-
quest_root=quest_root,
|
|
1409
|
-
reason=f"recent {stage} {kind} memory",
|
|
1410
|
-
)
|
|
1411
|
-
if len(selected) >= 6:
|
|
1412
|
-
return self._format_priority_memory(selected)
|
|
1413
|
-
|
|
1414
|
-
for query in self._memory_queries(user_message):
|
|
1415
|
-
matches = self.memory_service.search(query, scope="both", quest_root=quest_root, limit=6)
|
|
1416
|
-
for card in matches:
|
|
1417
|
-
scope = str(card.get("scope") or "quest")
|
|
1418
|
-
self._append_priority_memory(
|
|
1419
|
-
selected,
|
|
1420
|
-
seen_paths,
|
|
1421
|
-
card=card,
|
|
1422
|
-
scope=scope,
|
|
1423
|
-
quest_root=quest_root,
|
|
1424
|
-
reason=f"matches current user message: `{query}`",
|
|
1425
|
-
)
|
|
1426
|
-
if len(selected) >= 8:
|
|
1427
|
-
return self._format_priority_memory(selected)
|
|
1428
|
-
|
|
1429
|
-
return self._format_priority_memory(selected)
|
|
1213
|
+
quest_kinds = ", ".join(plan.get("quest", ())) or "none"
|
|
1214
|
+
global_kinds = ", ".join(plan.get("global", ())) or "none"
|
|
1215
|
+
return "\n".join(
|
|
1216
|
+
[
|
|
1217
|
+
f"- stage_memory_rule: for `{stage}`, prefer quest memory kinds [{quest_kinds}] and global memory kinds [{global_kinds}] when memory lookup is needed.",
|
|
1218
|
+
"- memory_lookup_tool: call memory.list_recent(...) to recover context after pause/restart and memory.search(...) before repeating prior work.",
|
|
1219
|
+
"- memory_injection_rule: memory is intentionally not pre-expanded here; pull only the cards that matter now.",
|
|
1220
|
+
]
|
|
1221
|
+
)
|
|
1430
1222
|
|
|
1431
1223
|
def _append_priority_memory(
|
|
1432
1224
|
self,
|
|
@@ -1488,20 +1280,12 @@ class PromptBuilder:
|
|
|
1488
1280
|
return tokens
|
|
1489
1281
|
|
|
1490
1282
|
def _conversation_block(self, quest_id: str, limit: int = 12) -> str:
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
source = str(item.get("source") or "unknown")
|
|
1498
|
-
content = str(item.get("content") or "").strip().replace("\n", " ")
|
|
1499
|
-
if len(content) > 400:
|
|
1500
|
-
content = content[:397].rstrip() + "..."
|
|
1501
|
-
reply_to = str(item.get("reply_to_interaction_id") or "").strip()
|
|
1502
|
-
suffix = f" -> reply_to:{reply_to}" if reply_to else ""
|
|
1503
|
-
lines.append(f"- [{role}|{source}]{suffix} {content}")
|
|
1504
|
-
return "\n".join(lines)
|
|
1283
|
+
return "\n".join(
|
|
1284
|
+
[
|
|
1285
|
+
"- conversation_context_rule: recent conversation is not pre-expanded here.",
|
|
1286
|
+
f"- conversation_tool: call artifact.get_conversation_context(limit={limit}, include_attachments=False) when earlier turn continuity matters.",
|
|
1287
|
+
]
|
|
1288
|
+
)
|
|
1505
1289
|
|
|
1506
1290
|
def _markdown_body(self, path: Path) -> str:
|
|
1507
1291
|
text = path.read_text(encoding="utf-8")
|