clouds-coder 2026.3.27__tar.gz → 2026.3.28__tar.gz
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.
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/Clouds_Coder.py +662 -87
- {clouds_coder-2026.3.27/clouds_coder.egg-info → clouds_coder-2026.3.28}/PKG-INFO +1 -1
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28/clouds_coder.egg-info}/PKG-INFO +1 -1
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/pyproject.toml +1 -1
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/LICENSE +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/README.md +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/setup.cfg +0 -0
- {clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/tests/test_smoke.py +0 -0
|
@@ -455,7 +455,7 @@ PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
|
|
|
455
455
|
TASK_PHASES = ("research", "design", "implement", "test", "review", "deploy")
|
|
456
456
|
TASK_PHASE_ROUTING = {
|
|
457
457
|
"research": "explorer",
|
|
458
|
-
"design": "
|
|
458
|
+
"design": "developer",
|
|
459
459
|
"implement": "developer",
|
|
460
460
|
"test": "developer",
|
|
461
461
|
"review": "reviewer",
|
|
@@ -480,6 +480,8 @@ EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before force
|
|
|
480
480
|
DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same file before forced strategy change
|
|
481
481
|
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 4096
|
|
482
482
|
PLAN_MODE_MAX_OPTIONS = 3
|
|
483
|
+
PLAN_FILE_RELATIVE_PATH = ".clouds_coder/plan.md"
|
|
484
|
+
PLAN_BUBBLE_MAX_CHARS = 3800 # margin under ASSISTANT_MESSAGE_EVENT_MAX_CHARS (4000)
|
|
483
485
|
PLAN_MODE_RESEARCH_TOOL_ALLOWLIST = {
|
|
484
486
|
"bash", "read_file", "context_recall", "task_get", "task_list",
|
|
485
487
|
"check_background", "read_from_blackboard", "write_to_blackboard",
|
|
@@ -4147,9 +4149,13 @@ class TodoManager:
|
|
|
4147
4149
|
row["owner"] = owner
|
|
4148
4150
|
if key:
|
|
4149
4151
|
row["key"] = key
|
|
4152
|
+
# Preserve parent_step_id for subtask-to-plan-step linkage
|
|
4153
|
+
parent_step_id = trim(str(raw.get("parent_step_id", "") or ""), 20)
|
|
4154
|
+
if parent_step_id:
|
|
4155
|
+
row["parent_step_id"] = parent_step_id
|
|
4150
4156
|
validated.append(row)
|
|
4151
|
-
if len(validated) >
|
|
4152
|
-
raise ValueError("max
|
|
4157
|
+
if len(validated) > 40:
|
|
4158
|
+
raise ValueError("max 40 todos")
|
|
4153
4159
|
if validated and not any(x["status"] == "in_progress" for x in validated):
|
|
4154
4160
|
for row in validated:
|
|
4155
4161
|
if row["status"] == "pending":
|
|
@@ -4792,6 +4798,7 @@ EMBEDDED_SKILLS_ARCHIVE_FILES = [
|
|
|
4792
4798
|
"skills/generated/upload-office-parser/SKILL.md",
|
|
4793
4799
|
"skills/generated/upload-parsers-capabilities.json",
|
|
4794
4800
|
"skills/generated/upload-tabular-parser/SKILL.md",
|
|
4801
|
+
"skills/generated/upload-image-parser/SKILL.md",
|
|
4795
4802
|
"skills/mcp-builder/SKILL.md",
|
|
4796
4803
|
"skills/pdf/SKILL.md",
|
|
4797
4804
|
"skills/skills_Gen/SKILL.md",
|
|
@@ -4950,6 +4957,36 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
|
|
|
4950
4957
|
## Notes
|
|
4951
4958
|
- The backend automatically parses `.doc`, `.docx`, `.ppt`, `.pptx`.
|
|
4952
4959
|
- If parser dependencies are unavailable, fallback extractor is used (may lose formatting).
|
|
4960
|
+
"""
|
|
4961
|
+
image_skill = """---
|
|
4962
|
+
name: upload-image-parser
|
|
4963
|
+
description: Analyze uploaded image files (PNG/JPG/JPEG/WEBP/GIF/BMP) using model native vision capabilities as the primary method; no OCR tools needed for supported formats.
|
|
4964
|
+
---
|
|
4965
|
+
|
|
4966
|
+
# Upload Image Parser
|
|
4967
|
+
|
|
4968
|
+
Use this skill when the user uploads image files and needs content description, analysis, extraction, or comparison.
|
|
4969
|
+
|
|
4970
|
+
## Primary Approach: Model Vision (Multimodal)
|
|
4971
|
+
1. Use `read_file` on the uploaded image path — the runtime automatically injects it as a native vision input to the model.
|
|
4972
|
+
2. Analyze or describe the image directly with vision capabilities; no external tools required.
|
|
4973
|
+
3. Uploaded image paths are under `files/uploaded/` in the session workspace. Check `Uploaded files context` in the system prompt for exact paths.
|
|
4974
|
+
|
|
4975
|
+
## Format Notes
|
|
4976
|
+
- Native formats (sent directly, no conversion): `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`
|
|
4977
|
+
- Auto-converted formats (runtime handles via Pillow): `.bmp`, `.tiff`, `.tif`, `.heic`, `.heif`, `.avif`
|
|
4978
|
+
- If conversion fails, the runtime returns an error message; use bash fallback below.
|
|
4979
|
+
- `.svg` files: runtime returns the SVG markup as text — parse the XML/SVG source directly, do not treat as a raster image.
|
|
4980
|
+
|
|
4981
|
+
## Fallback (only if runtime reports vision input unavailable)
|
|
4982
|
+
If the model cannot process the image natively (runtime message will say so):
|
|
4983
|
+
- OCR text extraction: `bash` → `tesseract <path> stdout`
|
|
4984
|
+
- Metadata / dimensions: `bash` → `identify <path>` (ImageMagick)
|
|
4985
|
+
- Pixel-level analysis: `bash` → `python3 -c "from PIL import Image; img=Image.open('<path>'); print(img.size, img.mode)"`
|
|
4986
|
+
|
|
4987
|
+
## Notes
|
|
4988
|
+
- Never attempt text extraction (OCR) on images when vision input is available — use the model's native understanding instead.
|
|
4989
|
+
- For multi-image comparison tasks, load each image via `read_file` sequentially; the runtime accumulates them as pending media inputs for the next model call.
|
|
4953
4990
|
"""
|
|
4954
4991
|
cap_json = json_dumps(
|
|
4955
4992
|
{
|
|
@@ -4960,6 +4997,7 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
|
|
|
4960
4997
|
)
|
|
4961
4998
|
_write_text_if_changed(generated_root / "upload-tabular-parser" / "SKILL.md", tabular_skill)
|
|
4962
4999
|
_write_text_if_changed(generated_root / "upload-office-parser" / "SKILL.md", office_skill)
|
|
5000
|
+
_write_text_if_changed(generated_root / "upload-image-parser" / "SKILL.md", image_skill)
|
|
4963
5001
|
_write_text_if_changed(generated_root / "upload-parsers-capabilities.json", cap_json)
|
|
4964
5002
|
|
|
4965
5003
|
def ensure_generated_image_coding_feedback_skill(skills_root: Path):
|
|
@@ -4982,12 +5020,12 @@ Use this skill when the task depends on image understanding in a coding workflow
|
|
|
4982
5020
|
- Generated image(s): current output from app, script, or model generation pipeline.
|
|
4983
5021
|
- Code scope: file paths, rendering command, and runtime constraints.
|
|
4984
5022
|
|
|
4985
|
-
##
|
|
4986
|
-
1.
|
|
4987
|
-
2.
|
|
4988
|
-
3.
|
|
5023
|
+
## Image Analysis: Vision First
|
|
5024
|
+
1. Load all reference and generated images via `read_file` — the runtime injects them as native vision inputs automatically.
|
|
5025
|
+
2. Analyze images directly with model vision capabilities; do not use OCR or pixel heuristics when vision input is available.
|
|
5026
|
+
3. Fallback (only if the runtime explicitly reports vision input unavailable):
|
|
4989
5027
|
- deterministic metadata checks (size/aspect/background),
|
|
4990
|
-
- text checks
|
|
5028
|
+
- text checks via OCR tools (e.g., `tesseract`),
|
|
4991
5029
|
- simple pixel-region checks from locally rendered output.
|
|
4992
5030
|
4. Always report confidence level (`high|medium|low`) based on signal quality.
|
|
4993
5031
|
|
|
@@ -6385,6 +6423,7 @@ def ensure_generated_runtime_skills_manifest(skills_root: Path):
|
|
|
6385
6423
|
"skills_Gen/knowledge_snapshot.json",
|
|
6386
6424
|
"generated/upload-tabular-parser/SKILL.md",
|
|
6387
6425
|
"generated/upload-office-parser/SKILL.md",
|
|
6426
|
+
"generated/upload-image-parser/SKILL.md",
|
|
6388
6427
|
"generated/image-coding-feedback-loop/SKILL.md",
|
|
6389
6428
|
"generated/execution-degradation-recovery/SKILL.md",
|
|
6390
6429
|
"generated/deep-research-orchestrator/SKILL.md",
|
|
@@ -9860,6 +9899,8 @@ class SessionState:
|
|
|
9860
9899
|
self.runtime_task_judgement = ""
|
|
9861
9900
|
self.runtime_task_type = ""
|
|
9862
9901
|
self.runtime_task_complexity = ""
|
|
9902
|
+
self.runtime_complexity_floor = ""
|
|
9903
|
+
self.runtime_task_level_floor = 0
|
|
9863
9904
|
self.runtime_scale_preference = "balanced"
|
|
9864
9905
|
self.runtime_direct_objective = ""
|
|
9865
9906
|
self.runtime_reclassify_goal = ""
|
|
@@ -10940,6 +10981,12 @@ class SessionState:
|
|
|
10940
10981
|
self.runtime_reclassify_required = bool(
|
|
10941
10982
|
raw.get("runtime_reclassify_required", self.runtime_reclassify_required)
|
|
10942
10983
|
)
|
|
10984
|
+
self.runtime_complexity_floor = str(
|
|
10985
|
+
raw.get("runtime_complexity_floor", self.runtime_complexity_floor) or ""
|
|
10986
|
+
)
|
|
10987
|
+
self.runtime_task_level_floor = int(
|
|
10988
|
+
raw.get("runtime_task_level_floor", self.runtime_task_level_floor) or 0
|
|
10989
|
+
)
|
|
10943
10990
|
self.runtime_goal_reset_pending = bool(
|
|
10944
10991
|
raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
|
|
10945
10992
|
)
|
|
@@ -11102,6 +11149,8 @@ class SessionState:
|
|
|
11102
11149
|
"runtime_direct_objective": trim(str(self.runtime_direct_objective or ""), 800),
|
|
11103
11150
|
"runtime_reclassify_goal": trim(str(self.runtime_reclassify_goal or ""), 4000),
|
|
11104
11151
|
"runtime_reclassify_required": bool(self.runtime_reclassify_required),
|
|
11152
|
+
"runtime_complexity_floor": str(self.runtime_complexity_floor or ""),
|
|
11153
|
+
"runtime_task_level_floor": int(self.runtime_task_level_floor or 0),
|
|
11105
11154
|
"runtime_goal_reset_pending": bool(self.runtime_goal_reset_pending),
|
|
11106
11155
|
"runtime_plan_mode_needed": bool(self.runtime_plan_mode_needed),
|
|
11107
11156
|
"runtime_plan_approved": bool(self.runtime_plan_approved),
|
|
@@ -11291,6 +11340,13 @@ class SessionState:
|
|
|
11291
11340
|
self.todo.items = []
|
|
11292
11341
|
except Exception:
|
|
11293
11342
|
pass
|
|
11343
|
+
# Clean up stale plan.md file from previous run
|
|
11344
|
+
try:
|
|
11345
|
+
_pf = self._plan_file_path()
|
|
11346
|
+
if _pf.exists():
|
|
11347
|
+
_pf.unlink()
|
|
11348
|
+
except Exception:
|
|
11349
|
+
pass
|
|
11294
11350
|
|
|
11295
11351
|
def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
|
|
11296
11352
|
payload = dict(data or {})
|
|
@@ -12477,7 +12533,7 @@ class SessionState:
|
|
|
12477
12533
|
o.pop("cons", None)
|
|
12478
12534
|
o.pop("risk", None)
|
|
12479
12535
|
elif tier >= 3:
|
|
12480
|
-
# Minimal: only phase, chosen, steps
|
|
12536
|
+
# Minimal: only phase, chosen, steps — but preserve project_todos
|
|
12481
12537
|
phase = plan.get("phase", "")
|
|
12482
12538
|
chosen = plan.get("chosen", "")
|
|
12483
12539
|
steps = plan.get("steps", [])
|
|
@@ -12493,7 +12549,10 @@ class SessionState:
|
|
|
12493
12549
|
o for o in options if isinstance(o, dict) and o.get("id") == chosen_id
|
|
12494
12550
|
]
|
|
12495
12551
|
if tier >= 3:
|
|
12496
|
-
|
|
12552
|
+
# Only clear proposal if plan is fully done
|
|
12553
|
+
plan_phase = str(bb.get("plan", {}).get("phase", ""))
|
|
12554
|
+
if plan_phase not in ("executing", "awaiting_choice"):
|
|
12555
|
+
self.runtime_plan_proposal = {}
|
|
12497
12556
|
|
|
12498
12557
|
def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
|
|
12499
12558
|
metrics = self._context_budget_metrics()
|
|
@@ -12895,16 +12954,13 @@ class SessionState:
|
|
|
12895
12954
|
meta = {"filtered": False, "reason": "", "original_chars": len(raw)}
|
|
12896
12955
|
if not clean:
|
|
12897
12956
|
return raw, meta
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
and len(clean) >= int(RAW_TOOLCALL_TEXT_FILTER_THRESHOLD)
|
|
12901
|
-
and self._looks_like_raw_toolcall_blob(clean)
|
|
12902
|
-
):
|
|
12957
|
+
# Filter raw <tool_call> XML regardless of size — any size is invalid display content
|
|
12958
|
+
if not tool_calls and self._looks_like_raw_toolcall_blob(clean):
|
|
12903
12959
|
note = (
|
|
12904
|
-
"[toolcall payload omitted: detected
|
|
12905
|
-
"
|
|
12960
|
+
"[toolcall payload omitted: detected inline <toolcall> text. "
|
|
12961
|
+
"Please regenerate a compact structured tool call.]"
|
|
12906
12962
|
)
|
|
12907
|
-
meta.update({"filtered": True, "reason": "
|
|
12963
|
+
meta.update({"filtered": True, "reason": "raw_toolcall"})
|
|
12908
12964
|
return note, meta
|
|
12909
12965
|
if len(raw) > int(ASSISTANT_TEXT_PERSIST_MAX_CHARS):
|
|
12910
12966
|
clipped = trim(raw, int(ASSISTANT_TEXT_PERSIST_MAX_CHARS))
|
|
@@ -19070,7 +19126,7 @@ class SessionState:
|
|
|
19070
19126
|
board["project_todos"] = []
|
|
19071
19127
|
else:
|
|
19072
19128
|
clean_todos = []
|
|
19073
|
-
for pt in bb_src_todos[:
|
|
19129
|
+
for pt in bb_src_todos[:40]:
|
|
19074
19130
|
if not isinstance(pt, dict):
|
|
19075
19131
|
continue
|
|
19076
19132
|
clean_todos.append({
|
|
@@ -19139,6 +19195,15 @@ class SessionState:
|
|
|
19139
19195
|
goal_preview = trim(str(src.get("loaded_skills_goal_preview", "") or ""), 240)
|
|
19140
19196
|
if goal_preview:
|
|
19141
19197
|
board["loaded_skills_goal_preview"] = goal_preview
|
|
19198
|
+
# Preserve step_files registry across normalization
|
|
19199
|
+
raw_step_files = src.get("step_files")
|
|
19200
|
+
if isinstance(raw_step_files, dict):
|
|
19201
|
+
clean_sf: dict[str, list] = {}
|
|
19202
|
+
for sf_key, sf_entries in raw_step_files.items():
|
|
19203
|
+
if isinstance(sf_entries, list):
|
|
19204
|
+
clean_sf[str(sf_key)] = sf_entries[-30:]
|
|
19205
|
+
if clean_sf:
|
|
19206
|
+
board["step_files"] = clean_sf
|
|
19142
19207
|
return board
|
|
19143
19208
|
|
|
19144
19209
|
def _normalize_failure_ledger(self, raw: dict) -> dict:
|
|
@@ -19566,9 +19631,17 @@ class SessionState:
|
|
|
19566
19631
|
self.blackboard["loaded_skills"] = preserved_skills
|
|
19567
19632
|
self.blackboard["loaded_skills_goal_sig"] = preserved_skills_sig
|
|
19568
19633
|
self.blackboard["loaded_skills_goal_preview"] = trim(str(goal or ""), 240)
|
|
19569
|
-
# Restore plan state if plan is
|
|
19570
|
-
|
|
19571
|
-
|
|
19634
|
+
# Restore plan state if plan is active (any phase) or todos have pending work
|
|
19635
|
+
has_active_plan = (
|
|
19636
|
+
isinstance(preserved_plan, dict)
|
|
19637
|
+
and preserved_plan.get("phase") in ("executing", "research", "synthesis", "awaiting_choice")
|
|
19638
|
+
)
|
|
19639
|
+
has_active_todos = isinstance(preserved_todos, list) and any(
|
|
19640
|
+
t.get("status") != "completed" for t in preserved_todos if isinstance(t, dict)
|
|
19641
|
+
)
|
|
19642
|
+
if has_active_plan or has_active_todos:
|
|
19643
|
+
if isinstance(preserved_plan, dict):
|
|
19644
|
+
self.blackboard["plan"] = preserved_plan
|
|
19572
19645
|
if isinstance(preserved_todos, list) and preserved_todos:
|
|
19573
19646
|
self.blackboard["project_todos"] = preserved_todos
|
|
19574
19647
|
if preserved_cursor is not None:
|
|
@@ -20131,14 +20204,19 @@ class SessionState:
|
|
|
20131
20204
|
})
|
|
20132
20205
|
self.blackboard = bb
|
|
20133
20206
|
self._blackboard_touch()
|
|
20134
|
-
#
|
|
20135
|
-
|
|
20207
|
+
# Persist step status change to plan.md
|
|
20208
|
+
try:
|
|
20209
|
+
self._update_plan_file_step_status()
|
|
20210
|
+
except Exception:
|
|
20211
|
+
pass # Plan file update is best-effort
|
|
20212
|
+
# 步骤推进时按 parent_step_id 清除对应步骤的 worker 子任务
|
|
20213
|
+
# 保留 completed 的 worker 项和不属于当前步骤的 worker 项
|
|
20214
|
+
completed_step_id = str(current.get("id", "") or "")
|
|
20136
20215
|
try:
|
|
20137
20216
|
_snap = self.todo.snapshot()
|
|
20138
|
-
_worker_owners = {"developer", "explorer", "reviewer"}
|
|
20139
20217
|
_clean = [
|
|
20140
20218
|
r for r in _snap
|
|
20141
|
-
if str(r.get("
|
|
20219
|
+
if str(r.get("parent_step_id", "") or "") != completed_step_id
|
|
20142
20220
|
or str(r.get("status", "") or "").lower() == "completed"
|
|
20143
20221
|
]
|
|
20144
20222
|
if len(_clean) < len(_snap):
|
|
@@ -20155,6 +20233,82 @@ class SessionState:
|
|
|
20155
20233
|
pass
|
|
20156
20234
|
return True
|
|
20157
20235
|
|
|
20236
|
+
def _post_execution_plan_step_check(self, route: dict, worker_step: dict):
|
|
20237
|
+
"""After worker execution, check if current plan step should advance based on evidence."""
|
|
20238
|
+
bb = self._ensure_blackboard()
|
|
20239
|
+
current = next(
|
|
20240
|
+
(t for t in bb.get("project_todos", [])
|
|
20241
|
+
if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
|
|
20242
|
+
None,
|
|
20243
|
+
)
|
|
20244
|
+
if not current:
|
|
20245
|
+
return
|
|
20246
|
+
# 1. Manager explicitly requested advancement
|
|
20247
|
+
manager_requested = bool(route.get("advance_plan_step_requested", False))
|
|
20248
|
+
# 2. Worker produced concrete tool outputs
|
|
20249
|
+
worker_produced_output = self._worker_step_has_evidence(worker_step)
|
|
20250
|
+
# 3. All subtasks for this step are completed
|
|
20251
|
+
subtasks_all_done = self._step_subtasks_all_completed(current)
|
|
20252
|
+
# Advance only when evidence confirms step completion:
|
|
20253
|
+
# - Manager requested AND worker produced output, OR
|
|
20254
|
+
# - All subtasks completed AND worker produced output
|
|
20255
|
+
has_strong_evidence = worker_produced_output and (
|
|
20256
|
+
manager_requested or subtasks_all_done
|
|
20257
|
+
)
|
|
20258
|
+
if has_strong_evidence:
|
|
20259
|
+
evidence = self._collect_step_evidence(current, worker_step)
|
|
20260
|
+
self._advance_plan_step(
|
|
20261
|
+
evidence=evidence,
|
|
20262
|
+
actor=str(route.get("target", "developer") or "developer"),
|
|
20263
|
+
)
|
|
20264
|
+
|
|
20265
|
+
def _worker_step_has_evidence(self, step: dict) -> bool:
|
|
20266
|
+
"""Check if worker step produced concrete tool outputs."""
|
|
20267
|
+
results = step.get("tool_results", []) or []
|
|
20268
|
+
return any(
|
|
20269
|
+
r.get("ok", False) and str(r.get("name", "")) in (
|
|
20270
|
+
"write_file", "edit_file", "bash", "read_file",
|
|
20271
|
+
"write_to_blackboard", "finish_current_task",
|
|
20272
|
+
)
|
|
20273
|
+
for r in results
|
|
20274
|
+
if isinstance(r, dict)
|
|
20275
|
+
)
|
|
20276
|
+
|
|
20277
|
+
def _step_subtasks_all_completed(self, plan_step: dict) -> bool:
|
|
20278
|
+
"""Check if all worker subtasks linked to this plan step are completed."""
|
|
20279
|
+
step_id = str(plan_step.get("id", "") or "")
|
|
20280
|
+
if not step_id:
|
|
20281
|
+
return False
|
|
20282
|
+
snap = self.todo.snapshot()
|
|
20283
|
+
worker_owners = {"developer", "explorer", "reviewer"}
|
|
20284
|
+
worker_items = [
|
|
20285
|
+
r for r in snap
|
|
20286
|
+
if str(r.get("owner", "") or "").lower() in worker_owners
|
|
20287
|
+
and str(r.get("parent_step_id", "") or "") == step_id
|
|
20288
|
+
]
|
|
20289
|
+
if not worker_items:
|
|
20290
|
+
return False
|
|
20291
|
+
return all(str(r.get("status", "")).lower() == "completed" for r in worker_items)
|
|
20292
|
+
|
|
20293
|
+
def _collect_step_evidence(self, plan_step: dict, worker_step: dict) -> str:
|
|
20294
|
+
"""Collect evidence summary from worker step for plan step completion."""
|
|
20295
|
+
parts = []
|
|
20296
|
+
results = worker_step.get("tool_results", []) or []
|
|
20297
|
+
for r in results:
|
|
20298
|
+
if not isinstance(r, dict) or not r.get("ok", False):
|
|
20299
|
+
continue
|
|
20300
|
+
name = str(r.get("name", ""))
|
|
20301
|
+
if name in ("write_file", "edit_file"):
|
|
20302
|
+
path = str(r.get("args", {}).get("path", "") or "")
|
|
20303
|
+
parts.append(f"{name}: {path}")
|
|
20304
|
+
elif name == "bash":
|
|
20305
|
+
cmd = trim(str(r.get("args", {}).get("command", "") or ""), 80)
|
|
20306
|
+
parts.append(f"bash: {cmd}")
|
|
20307
|
+
elif name == "read_file":
|
|
20308
|
+
path = str(r.get("args", {}).get("path", "") or "")
|
|
20309
|
+
parts.append(f"read: {path}")
|
|
20310
|
+
return trim("; ".join(parts) or "post-execution evidence", 200)
|
|
20311
|
+
|
|
20158
20312
|
def _single_agent_plan_step_check(self, tool_results: list[dict]):
|
|
20159
20313
|
"""In single-agent mode, check if current plan step should be advanced based on tool results."""
|
|
20160
20314
|
bb = self._ensure_blackboard()
|
|
@@ -20228,6 +20382,7 @@ class SessionState:
|
|
|
20228
20382
|
_step_label = f"Step {_step_idx}" + (f"/{_total}" if _total else "")
|
|
20229
20383
|
_hint = (
|
|
20230
20384
|
f"[plan-step-advance] Previous step completed. Now at {_step_label}: {_step_text}\n"
|
|
20385
|
+
f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
|
|
20231
20386
|
"Call TodoWrite to set your task breakdown for this step "
|
|
20232
20387
|
"(3-5 subtask items, one marked in_progress) before proceeding."
|
|
20233
20388
|
)
|
|
@@ -20285,8 +20440,13 @@ class SessionState:
|
|
|
20285
20440
|
if is_system_key or owner == "manager":
|
|
20286
20441
|
continue
|
|
20287
20442
|
non_system_rows.append(dict(row))
|
|
20288
|
-
|
|
20289
|
-
|
|
20443
|
+
# Smart trim: keep all active (in_progress/pending) system rows,
|
|
20444
|
+
# but only recent 3 completed system rows to save capacity for worker subtasks
|
|
20445
|
+
active_system = [r for r in system_rows if r.get("status") != "completed"]
|
|
20446
|
+
completed_system = [r for r in system_rows if r.get("status") == "completed"]
|
|
20447
|
+
trimmed_system = active_system + completed_system[-3:]
|
|
20448
|
+
remaining_cap = max(0, 40 - len(trimmed_system) - len(worker_rows))
|
|
20449
|
+
merged = list(trimmed_system) + worker_rows + non_system_rows[:remaining_cap]
|
|
20290
20450
|
try:
|
|
20291
20451
|
todo_out = self.todo.update(merged)
|
|
20292
20452
|
except Exception:
|
|
@@ -21441,7 +21601,8 @@ class SessionState:
|
|
|
21441
21601
|
todos = bb.get("project_todos", [])
|
|
21442
21602
|
if not todos or not any(t.get("category") == "plan_step" for t in todos):
|
|
21443
21603
|
return ""
|
|
21444
|
-
lines = ["
|
|
21604
|
+
lines = [f"PLAN FILE: {PLAN_FILE_RELATIVE_PATH} (read_file for full plan with live status)",
|
|
21605
|
+
"APPROVED PLAN STEPS:"]
|
|
21445
21606
|
for t in todos:
|
|
21446
21607
|
if t.get("category") != "plan_step":
|
|
21447
21608
|
continue
|
|
@@ -21453,10 +21614,14 @@ class SessionState:
|
|
|
21453
21614
|
lines.append(f" {mark} Step {idx}: {trim(str(t.get('content', '') or ''), 160)}{phase_tag}")
|
|
21454
21615
|
lines.append("Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing. ")
|
|
21455
21616
|
lines.append(
|
|
21456
|
-
"STEP COMPLETION RULE: Set advance_plan_step=true when
|
|
21457
|
-
"
|
|
21458
|
-
"
|
|
21459
|
-
"
|
|
21617
|
+
"STEP COMPLETION RULE: Set advance_plan_step=true ONLY when:\n"
|
|
21618
|
+
" 1. The worker has ALREADY executed tools and produced verifiable output "
|
|
21619
|
+
"(NOT when dispatching the work for the first time), AND\n"
|
|
21620
|
+
" 2. At least ONE of: research_notes populated, code_artifacts created, "
|
|
21621
|
+
"bash execution succeeded, or all worker TodoWrite subtasks (with parent_step_id) completed.\n"
|
|
21622
|
+
" NEVER set advance_plan_step=true in the SAME delegation that assigns the work. "
|
|
21623
|
+
"The step must have been executed FIRST.\n"
|
|
21624
|
+
"COMPLEXITY LOCK: Do NOT change complexity or task_level below the plan-approved levels. "
|
|
21460
21625
|
)
|
|
21461
21626
|
lines.append("MANDATORY: Your delegation instruction MUST reference the current plan step. "
|
|
21462
21627
|
"Do NOT reinterpret or replace plan steps with your own objectives. ")
|
|
@@ -21485,12 +21650,15 @@ class SessionState:
|
|
|
21485
21650
|
def _plan_step_phase_hint(self, step_content: str) -> str:
|
|
21486
21651
|
"""Infer the task phase from a plan step's content."""
|
|
21487
21652
|
c = str(step_content or "").lower()
|
|
21653
|
+
# implement keywords take priority — if step produces output, it's implement not design/research
|
|
21654
|
+
if any(kw in c for kw in ("实现", "编写", "创建", "开发", "绘制", "生成", "写入",
|
|
21655
|
+
"implement", "write", "create", "build", "develop", "code",
|
|
21656
|
+
"generate", "draw", "scaffold", "mkdir", ".f90", ".py", ".cpp", ".md")):
|
|
21657
|
+
return "implement"
|
|
21488
21658
|
if any(kw in c for kw in ("研究", "分析", "调研", "探索", "research", "analyze", "investigate", "explore", "inspect")):
|
|
21489
21659
|
return "research"
|
|
21490
21660
|
if any(kw in c for kw in ("设计", "架构", "规划", "design", "architect", "plan", "interface", "接口")):
|
|
21491
21661
|
return "design"
|
|
21492
|
-
if any(kw in c for kw in ("实现", "编写", "创建", "开发", "implement", "write", "create", "build", "develop", "code")):
|
|
21493
|
-
return "implement"
|
|
21494
21662
|
if any(kw in c for kw in ("测试", "验证", "检查", "test", "verify", "check", "validate", "compile")):
|
|
21495
21663
|
return "test"
|
|
21496
21664
|
if any(kw in c for kw in ("审查", "评审", "review", "audit", "inspect code")):
|
|
@@ -21553,11 +21721,24 @@ class SessionState:
|
|
|
21553
21721
|
completed_w = [r for r in worker_todos if str(r.get("status", "")).lower() == "completed"]
|
|
21554
21722
|
pending_w = [r for r in worker_todos if str(r.get("status", "")).lower() not in ("completed",)]
|
|
21555
21723
|
if not pending_w and completed_w:
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
|
|
21560
|
-
)
|
|
21724
|
+
# Verify actual work evidence before suggesting advance
|
|
21725
|
+
bb_now = self._ensure_blackboard()
|
|
21726
|
+
has_artifacts = bool(bb_now.get("code_artifacts"))
|
|
21727
|
+
has_research = bool(bb_now.get("research_notes"))
|
|
21728
|
+
has_shell_output = bool(bb_now.get("execution_logs"))
|
|
21729
|
+
has_evidence = has_artifacts or has_research or has_shell_output
|
|
21730
|
+
if has_evidence:
|
|
21731
|
+
worker_hint = (
|
|
21732
|
+
f"Worker subtasks: all {len(completed_w)} completed "
|
|
21733
|
+
f"({', '.join(trim(str(r.get('content','') or ''), 40) for r in completed_w[:3])}). "
|
|
21734
|
+
"Blackboard has concrete outputs. \u2192 Set advance_plan_step=true NOW. "
|
|
21735
|
+
)
|
|
21736
|
+
else:
|
|
21737
|
+
worker_hint = (
|
|
21738
|
+
f"Worker subtasks: all {len(completed_w)} marked completed "
|
|
21739
|
+
"but blackboard has NO concrete outputs (no code_artifacts, research_notes, or execution_logs). "
|
|
21740
|
+
"\u2192 Do NOT advance. Re-delegate to verify actual work was done. "
|
|
21741
|
+
)
|
|
21561
21742
|
elif completed_w or pending_w:
|
|
21562
21743
|
worker_hint = (
|
|
21563
21744
|
f"Worker subtasks: {len(completed_w)} done, {len(pending_w)} pending. "
|
|
@@ -22811,7 +22992,7 @@ class SessionState:
|
|
|
22811
22992
|
prompt = (
|
|
22812
22993
|
"Read the blackboard and delegate one next short timeslice. "
|
|
22813
22994
|
"Return only one route_to_next_agent call.\n\n"
|
|
22814
|
-
f"{self._blackboard_read_state_markdown(max_items=
|
|
22995
|
+
f"{self._blackboard_read_state_markdown(max_items=10)}"
|
|
22815
22996
|
)
|
|
22816
22997
|
self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
|
|
22817
22998
|
self._microcompact_agent_messages(self.manager_context)
|
|
@@ -23010,21 +23191,12 @@ class SessionState:
|
|
|
23010
23191
|
"round_budget": int(round_budget),
|
|
23011
23192
|
"remaining_rounds": int(remaining_rounds),
|
|
23012
23193
|
}
|
|
23013
|
-
# advance_plan_step:
|
|
23014
|
-
#
|
|
23015
|
-
#
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
str(route.get("reason", "") or ""),
|
|
23194
|
+
# advance_plan_step: REMOVED pre-execution advancement.
|
|
23195
|
+
# Step advancement now happens AFTER worker execution via _post_execution_plan_step_check.
|
|
23196
|
+
# Store the manager's advance request flag for post-execution validation.
|
|
23197
|
+
route_row["advance_plan_step_requested"] = _to_bool_like(
|
|
23198
|
+
route.get("advance_plan_step", False), default=False
|
|
23019
23199
|
)
|
|
23020
|
-
if should_advance:
|
|
23021
|
-
self._advance_plan_step(
|
|
23022
|
-
evidence=trim(str(route.get("instruction", "") or ""), 200),
|
|
23023
|
-
actor=str(route.get("target", "developer") or "developer"),
|
|
23024
|
-
)
|
|
23025
|
-
# CRITICAL: re-anchor board to the updated blackboard so the
|
|
23026
|
-
# self.blackboard = board at line ~22388 does NOT overwrite the advance.
|
|
23027
|
-
board = self.blackboard
|
|
23028
23200
|
self.manager_routes.append(route_row)
|
|
23029
23201
|
self.manager_routes = self.manager_routes[-240:]
|
|
23030
23202
|
# Failure ledger: persist route and record delegation
|
|
@@ -23041,7 +23213,11 @@ class SessionState:
|
|
|
23041
23213
|
self.blackboard = board
|
|
23042
23214
|
self._ledger_record_delegation(target, instruction)
|
|
23043
23215
|
profile = self._ensure_blackboard_task_profile(board)
|
|
23044
|
-
|
|
23216
|
+
# Complexity / task_level floor protection: prevent manager downgrade during plan execution
|
|
23217
|
+
effective_level = int(task_level)
|
|
23218
|
+
if int(self.runtime_task_level_floor or 0) > 0:
|
|
23219
|
+
effective_level = max(effective_level, int(self.runtime_task_level_floor))
|
|
23220
|
+
profile["task_level"] = effective_level
|
|
23045
23221
|
profile["execution_mode"] = execution_mode
|
|
23046
23222
|
profile["participants"] = list(participants)
|
|
23047
23223
|
profile["assigned_expert"] = assigned_expert
|
|
@@ -23051,6 +23227,9 @@ class SessionState:
|
|
|
23051
23227
|
if task_type in TASK_PROFILE_TYPES:
|
|
23052
23228
|
profile["task_type"] = task_type
|
|
23053
23229
|
if complexity in TASK_COMPLEXITY_LEVELS:
|
|
23230
|
+
# Floor protection: if plan mode set a floor, do not allow downgrade
|
|
23231
|
+
if self.runtime_complexity_floor == "complex" and complexity == "simple":
|
|
23232
|
+
complexity = "complex"
|
|
23054
23233
|
profile["complexity"] = complexity
|
|
23055
23234
|
profile["scale_preference"] = scale_preference if scale_preference in TASK_SCALE_PREFERENCES else "balanced"
|
|
23056
23235
|
if objective:
|
|
@@ -23258,7 +23437,7 @@ class SessionState:
|
|
|
23258
23437
|
"Do NOT repeat previous failed fixes.\n"
|
|
23259
23438
|
"</compile-error-context>\n"
|
|
23260
23439
|
)
|
|
23261
|
-
board_md = self._blackboard_read_state_markdown(max_items=
|
|
23440
|
+
board_md = self._blackboard_read_state_markdown(max_items=10)
|
|
23262
23441
|
# Include loaded skills hint in delegation
|
|
23263
23442
|
loaded_skills_note = self._loaded_skills_prompt_hint(for_role=role_key)
|
|
23264
23443
|
# Include current plan step in delegation so agents stay on track
|
|
@@ -23278,11 +23457,46 @@ class SessionState:
|
|
|
23278
23457
|
# developer 被调用时有活跃 plan step,要求在开始前调用 TodoWrite 刷新子任务
|
|
23279
23458
|
todo_update_note = ""
|
|
23280
23459
|
if role_key == "developer" and current_plan_step_note:
|
|
23460
|
+
# Extract step_id from the matched plan step for parent_step_id linkage
|
|
23461
|
+
_active_step_id = ""
|
|
23462
|
+
if isinstance(plan_todos, list):
|
|
23463
|
+
for _pt in plan_todos:
|
|
23464
|
+
if isinstance(_pt, dict) and _pt.get("category") == "plan_step" and _pt.get("status") == "in_progress":
|
|
23465
|
+
_active_step_id = str(_pt.get("id", "") or "")
|
|
23466
|
+
break
|
|
23281
23467
|
todo_update_note = (
|
|
23282
|
-
"TODO UPDATE:
|
|
23283
|
-
"
|
|
23284
|
-
"
|
|
23285
|
-
|
|
23468
|
+
f"TODO UPDATE: At the START of your work, call TodoWrite to set subtasks for this step.\n"
|
|
23469
|
+
f"Each subtask MUST include parent_step_id='{_active_step_id}' to link it to this plan step.\n"
|
|
23470
|
+
f"Format: 3-5 items, one marked in_progress, others pending.\n"
|
|
23471
|
+
f"Mark each subtask completed as you finish it. When ALL subtasks are done, the step auto-advances.\n"
|
|
23472
|
+
)
|
|
23473
|
+
# Build step_files context note for cross-agent file visibility
|
|
23474
|
+
step_files_note = ""
|
|
23475
|
+
if current_plan_step_note:
|
|
23476
|
+
try:
|
|
23477
|
+
_sf_bb = self._ensure_blackboard()
|
|
23478
|
+
_sf_data = _sf_bb.get("step_files", {})
|
|
23479
|
+
_sf_step_id = ""
|
|
23480
|
+
if isinstance(plan_todos, list):
|
|
23481
|
+
for _sfpt in plan_todos:
|
|
23482
|
+
if isinstance(_sfpt, dict) and _sfpt.get("category") == "plan_step" and _sfpt.get("status") == "in_progress":
|
|
23483
|
+
_sf_step_id = str(_sfpt.get("id", "") or "")
|
|
23484
|
+
break
|
|
23485
|
+
if _sf_step_id and isinstance(_sf_data, dict):
|
|
23486
|
+
_sf_entries = _sf_data.get(_sf_step_id, [])
|
|
23487
|
+
if _sf_entries:
|
|
23488
|
+
_sf_paths = list(dict.fromkeys(
|
|
23489
|
+
str(e.get("path", "") or "")
|
|
23490
|
+
for e in _sf_entries[-20:]
|
|
23491
|
+
if isinstance(e, dict) and e.get("path")
|
|
23492
|
+
))
|
|
23493
|
+
if _sf_paths:
|
|
23494
|
+
step_files_note = (
|
|
23495
|
+
"FILES ACCESSED IN THIS STEP:\n"
|
|
23496
|
+
+ "\n".join(f"- {p}" for p in _sf_paths[:15]) + "\n"
|
|
23497
|
+
)
|
|
23498
|
+
except Exception:
|
|
23499
|
+
pass
|
|
23286
23500
|
payload = (
|
|
23287
23501
|
"<manager-delegate>\n"
|
|
23288
23502
|
f"target={role_key}\n"
|
|
@@ -23297,6 +23511,7 @@ class SessionState:
|
|
|
23297
23511
|
f"{loaded_skills_note}"
|
|
23298
23512
|
f"{current_plan_step_note}"
|
|
23299
23513
|
f"{todo_update_note}"
|
|
23514
|
+
f"{step_files_note}"
|
|
23300
23515
|
f"{error_section}"
|
|
23301
23516
|
"</manager-delegate>\n"
|
|
23302
23517
|
"<blackboard-state>\n"
|
|
@@ -23404,6 +23619,32 @@ class SessionState:
|
|
|
23404
23619
|
role_key,
|
|
23405
23620
|
f"tool_error {name}: {output}",
|
|
23406
23621
|
)
|
|
23622
|
+
# Track file operations in step_files registry for cross-agent context
|
|
23623
|
+
if name in ("read_file", "write_file", "edit_file"):
|
|
23624
|
+
file_path = trim(str(args.get("path", "") or "").strip(), 240)
|
|
23625
|
+
if file_path:
|
|
23626
|
+
try:
|
|
23627
|
+
bb = self._ensure_blackboard()
|
|
23628
|
+
current_step = next(
|
|
23629
|
+
(t for t in bb.get("project_todos", [])
|
|
23630
|
+
if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
|
|
23631
|
+
None,
|
|
23632
|
+
)
|
|
23633
|
+
if current_step:
|
|
23634
|
+
step_id = str(current_step.get("id", "") or "")
|
|
23635
|
+
if step_id:
|
|
23636
|
+
sf = bb.setdefault("step_files", {})
|
|
23637
|
+
entries = sf.setdefault(step_id, [])
|
|
23638
|
+
entries.append({
|
|
23639
|
+
"path": file_path,
|
|
23640
|
+
"role": role_key,
|
|
23641
|
+
"op": name,
|
|
23642
|
+
"ts": float(now_ts()),
|
|
23643
|
+
})
|
|
23644
|
+
if len(entries) > 30:
|
|
23645
|
+
sf[step_id] = entries[-30:]
|
|
23646
|
+
except Exception:
|
|
23647
|
+
pass
|
|
23407
23648
|
|
|
23408
23649
|
def _blackboard_update_from_worker_step(self, role: str, step: dict):
|
|
23409
23650
|
role_key = self._sanitize_agent_role(role)
|
|
@@ -25722,8 +25963,13 @@ class SessionState:
|
|
|
25722
25963
|
self.runtime_plan_mode_needed = True
|
|
25723
25964
|
# Reset completed plan/todo/skills blackboard state so the manager
|
|
25724
25965
|
# does not see status=COMPLETED on the very first round and immediately finish.
|
|
25966
|
+
# But preserve plan state if user is continuing an existing task.
|
|
25725
25967
|
if not _awaiting_plan_choice:
|
|
25726
|
-
|
|
25968
|
+
clean_goal_pre = trim(str(content or "").strip(), 4000)
|
|
25969
|
+
if self._is_continuation_input(clean_goal_pre):
|
|
25970
|
+
pass # Preserve plan state for continuation
|
|
25971
|
+
else:
|
|
25972
|
+
self._reset_blackboard_plan_state_locked()
|
|
25727
25973
|
self.run_generation = int(self.run_generation) + 1
|
|
25728
25974
|
clean_goal = trim(str(content or "").strip(), 4000)
|
|
25729
25975
|
self._refresh_runtime_code_reference(clean_goal or content)
|
|
@@ -26330,6 +26576,8 @@ class SessionState:
|
|
|
26330
26576
|
media_inputs_round=role_media_inputs,
|
|
26331
26577
|
)
|
|
26332
26578
|
self._blackboard_update_from_worker_step(role, step)
|
|
26579
|
+
# Post-execution plan step advancement (replaces pre-execution advancement)
|
|
26580
|
+
self._post_execution_plan_step_check(route, step if isinstance(step, dict) else {})
|
|
26333
26581
|
# ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
|
|
26334
26582
|
agent_text = self._latest_agent_assistant_text(role)
|
|
26335
26583
|
if (
|
|
@@ -26626,6 +26874,15 @@ class SessionState:
|
|
|
26626
26874
|
self._plan_mode_update_findings(step)
|
|
26627
26875
|
|
|
26628
26876
|
# Phase 2: Manager 综合分析
|
|
26877
|
+
# Inject pending user inputs before synthesis
|
|
26878
|
+
self._inject_pending_user_inputs()
|
|
26879
|
+
# Check if user sent a substantive goal change during research
|
|
26880
|
+
if self._has_pending_goal_change():
|
|
26881
|
+
self._emit("status", {"summary": "plan-mode: user changed task during research, restarting"})
|
|
26882
|
+
self.runtime_plan_mode_needed = False
|
|
26883
|
+
self.runtime_plan_approved = False
|
|
26884
|
+
return
|
|
26885
|
+
|
|
26629
26886
|
self._emit("status", {"summary": "plan-mode: synthesizing proposals"})
|
|
26630
26887
|
bb = self._ensure_blackboard()
|
|
26631
26888
|
if not isinstance(bb.get("plan"), dict):
|
|
@@ -26633,7 +26890,17 @@ class SessionState:
|
|
|
26633
26890
|
bb["plan"]["phase"] = "synthesis"
|
|
26634
26891
|
self.blackboard = bb
|
|
26635
26892
|
|
|
26636
|
-
|
|
26893
|
+
# Synthesis with retry (up to 2 attempts) + minimal fallback
|
|
26894
|
+
proposal = None
|
|
26895
|
+
for _synth_attempt in range(2):
|
|
26896
|
+
proposal = self._plan_mode_synthesize_proposal(pinned_selection)
|
|
26897
|
+
if proposal and proposal.get("options"):
|
|
26898
|
+
break
|
|
26899
|
+
if _synth_attempt == 0:
|
|
26900
|
+
self._emit("status", {"summary": "plan-mode: synthesis retry"})
|
|
26901
|
+
if not proposal or not proposal.get("options"):
|
|
26902
|
+
# Last resort: minimal fallback with simpler prompt and higher token budget
|
|
26903
|
+
proposal = self._synthesis_minimal_fallback(pinned_selection)
|
|
26637
26904
|
if not proposal or not proposal.get("options"):
|
|
26638
26905
|
self._emit("status", {"summary": "plan-mode: synthesis failed, falling back to direct execution"})
|
|
26639
26906
|
self.runtime_plan_mode_needed = False
|
|
@@ -26649,16 +26916,23 @@ class SessionState:
|
|
|
26649
26916
|
bb["plan"]["proposal"] = proposal
|
|
26650
26917
|
self.blackboard = bb
|
|
26651
26918
|
|
|
26652
|
-
|
|
26919
|
+
# Write full plan to file for model consumption
|
|
26920
|
+
try:
|
|
26921
|
+
self._write_plan_file(self._format_plan_file_preselection(proposal))
|
|
26922
|
+
except Exception:
|
|
26923
|
+
pass
|
|
26924
|
+
|
|
26925
|
+
# Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS)
|
|
26926
|
+
bubble_text = self._format_plan_bubble_preselection(proposal)
|
|
26653
26927
|
self.messages.append({
|
|
26654
26928
|
"role": "assistant",
|
|
26655
|
-
"content":
|
|
26929
|
+
"content": bubble_text,
|
|
26656
26930
|
"ts": now_ts(),
|
|
26657
26931
|
"agent_role": "planner",
|
|
26658
26932
|
})
|
|
26659
26933
|
self._emit("message", {
|
|
26660
26934
|
"role": "assistant",
|
|
26661
|
-
"text": trim(
|
|
26935
|
+
"text": trim(bubble_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
|
|
26662
26936
|
"summary": "plan-mode proposal",
|
|
26663
26937
|
"agent_role": "planner",
|
|
26664
26938
|
})
|
|
@@ -26706,7 +26980,12 @@ class SessionState:
|
|
|
26706
26980
|
f" - Relevant skills found (names, what they do, how to invoke them)\n"
|
|
26707
26981
|
f" - File inventory (uploaded files, their types, sizes, key content)\n"
|
|
26708
26982
|
f" - Skill workflow breakdown (concrete tools, scripts, paths for each relevant skill)\n"
|
|
26709
|
-
f" - Content analysis (key themes, structure, data points extracted from inputs)\n
|
|
26983
|
+
f" - Content analysis (key themes, structure, data points extracted from inputs)\n"
|
|
26984
|
+
f"9. For coding tasks, identify the test strategy:\n"
|
|
26985
|
+
f" - What build/compilation commands are available? (Makefile, npm, cargo, cmake, etc.)\n"
|
|
26986
|
+
f" - What test frameworks/suites exist? (pytest, jest, go test, etc.)\n"
|
|
26987
|
+
f" - What are the critical paths that must be tested?\n"
|
|
26988
|
+
f" - Record these in plan_findings under 'test_strategy'.\n\n"
|
|
26710
26989
|
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT)\n"
|
|
26711
26990
|
f"{os_note}\n"
|
|
26712
26991
|
f"{lang_note}"
|
|
@@ -27168,8 +27447,17 @@ class SessionState:
|
|
|
27168
27447
|
f"- Each step should be completable in 1-3 tool calls\n"
|
|
27169
27448
|
f"- Group related substeps under numbered headings (e.g., '2.1 Read report 1', '2.2 Read report 2')\n"
|
|
27170
27449
|
f"Make options meaningfully different (e.g. different approaches, scope levels, or trade-offs).\n"
|
|
27171
|
-
|
|
27172
|
-
|
|
27450
|
+
"\nVERIFICATION & TESTING:\n"
|
|
27451
|
+
"Judge from the task content and research findings whether the task involves writing, "
|
|
27452
|
+
"modifying, or generating code/scripts/configurations. If it does:\n"
|
|
27453
|
+
"- Include compile/build/lint verification steps after implementation steps.\n"
|
|
27454
|
+
"- Include a dedicated testing step with specific commands before final review.\n"
|
|
27455
|
+
"- For large plans (10+ steps), insert intermediate test checkpoints.\n"
|
|
27456
|
+
"- If the task modifies existing code, include a regression test step.\n"
|
|
27457
|
+
"If the task is pure research, analysis, or document generation with no executable code, "
|
|
27458
|
+
"skip compile/test steps — use your judgement.\n"
|
|
27459
|
+
)
|
|
27460
|
+
synthesis_prompt += f"{model_language_instruction(self.ui_language)}"
|
|
27173
27461
|
synthesis_ctx = [
|
|
27174
27462
|
{"role": "system", "content": (
|
|
27175
27463
|
"You are a technical architect synthesizing research into actionable plans. "
|
|
@@ -27198,6 +27486,63 @@ class SessionState:
|
|
|
27198
27486
|
return dict(args)
|
|
27199
27487
|
return {}
|
|
27200
27488
|
|
|
27489
|
+
def _synthesis_minimal_fallback(self, pinned_selection: str) -> dict:
|
|
27490
|
+
"""Last-resort: ask model for a single simple plan with higher max_tokens."""
|
|
27491
|
+
goal = trim(str(self.runtime_reclassify_goal or self._latest_user_goal_text() or ""), 2000)
|
|
27492
|
+
bb = self._ensure_blackboard()
|
|
27493
|
+
findings = bb.get("plan", {}).get("findings", []) if isinstance(bb.get("plan"), dict) else []
|
|
27494
|
+
findings_text = "\n".join(
|
|
27495
|
+
trim(str(f.get("content", "") if isinstance(f, dict) else ""), 600)
|
|
27496
|
+
for f in (findings[:5] if isinstance(findings, list) else [])
|
|
27497
|
+
)
|
|
27498
|
+
prompt = (
|
|
27499
|
+
f"Generate ONE simple plan for this task. Call submit_plan_proposal with exactly 1 option.\n\n"
|
|
27500
|
+
f"Task: {goal}\n\nFindings: {trim(findings_text, 3000)}\n\n"
|
|
27501
|
+
f"Return a single option with id='A', title, summary, and 5-10 concrete steps.\n"
|
|
27502
|
+
f"{model_language_instruction(self.ui_language)}"
|
|
27503
|
+
)
|
|
27504
|
+
ctx = [
|
|
27505
|
+
{"role": "system", "content": "You must call submit_plan_proposal tool.", "ts": now_ts()},
|
|
27506
|
+
{"role": "user", "content": prompt, "ts": now_ts()},
|
|
27507
|
+
]
|
|
27508
|
+
try:
|
|
27509
|
+
response = self._chat_with_same_model_retry(
|
|
27510
|
+
ctx,
|
|
27511
|
+
tools=self._plan_mode_synthesis_tools(),
|
|
27512
|
+
system="Call submit_plan_proposal now.",
|
|
27513
|
+
max_tokens=6000,
|
|
27514
|
+
think=False,
|
|
27515
|
+
stream_thinking=False,
|
|
27516
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
27517
|
+
pinned_selection=pinned_selection,
|
|
27518
|
+
context_label="plan-mode minimal fallback",
|
|
27519
|
+
retries=2,
|
|
27520
|
+
)
|
|
27521
|
+
for tc in response.get("tool_calls", []):
|
|
27522
|
+
if tc.get("function", {}).get("name") == "submit_plan_proposal":
|
|
27523
|
+
args = tc["function"].get("arguments", {})
|
|
27524
|
+
if isinstance(args, dict) and args.get("options"):
|
|
27525
|
+
return dict(args)
|
|
27526
|
+
except Exception:
|
|
27527
|
+
pass
|
|
27528
|
+
return {}
|
|
27529
|
+
|
|
27530
|
+
def _has_pending_goal_change(self) -> bool:
|
|
27531
|
+
"""Check if user sent a substantive goal change (not just plan choice or continuation)."""
|
|
27532
|
+
with self.lock:
|
|
27533
|
+
for row in self.pending_user_inputs:
|
|
27534
|
+
content = str(row.get("content", "") or "").strip().lower()
|
|
27535
|
+
if not content:
|
|
27536
|
+
continue
|
|
27537
|
+
if content in {
|
|
27538
|
+
"继续", "continue", "go on", "接着", "a", "b", "c",
|
|
27539
|
+
"方案a", "方案b", "方案c", "keep going", "proceed",
|
|
27540
|
+
}:
|
|
27541
|
+
continue
|
|
27542
|
+
if len(content) > 10:
|
|
27543
|
+
return True
|
|
27544
|
+
return False
|
|
27545
|
+
|
|
27201
27546
|
def _plan_mode_synthesis_tools(self) -> list:
|
|
27202
27547
|
return [tool_def(
|
|
27203
27548
|
"submit_plan_proposal",
|
|
@@ -27225,6 +27570,185 @@ class SessionState:
|
|
|
27225
27570
|
["context", "options", "recommended"],
|
|
27226
27571
|
)]
|
|
27227
27572
|
|
|
27573
|
+
# ── Plan MD File helpers ──────────────────────────────────────────
|
|
27574
|
+
|
|
27575
|
+
def _plan_file_path(self) -> Path:
|
|
27576
|
+
"""Plan MD file path (inside files_root sandbox, accessible via read_file)."""
|
|
27577
|
+
return self.files_root / ".clouds_coder" / "plan.md"
|
|
27578
|
+
|
|
27579
|
+
def _write_plan_file(self, content: str) -> bool:
|
|
27580
|
+
"""Atomically write plan.md: write tmp → os.replace."""
|
|
27581
|
+
import uuid as _uuid_mod
|
|
27582
|
+
target = self._plan_file_path()
|
|
27583
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
27584
|
+
tmp = target.with_suffix(f".{_uuid_mod.uuid4().hex[:8]}.tmp")
|
|
27585
|
+
try:
|
|
27586
|
+
tmp.write_text(content, encoding="utf-8")
|
|
27587
|
+
os.replace(str(tmp), str(target))
|
|
27588
|
+
return True
|
|
27589
|
+
except Exception:
|
|
27590
|
+
try:
|
|
27591
|
+
tmp.unlink(missing_ok=True)
|
|
27592
|
+
except Exception:
|
|
27593
|
+
pass
|
|
27594
|
+
return False
|
|
27595
|
+
|
|
27596
|
+
def _read_plan_file(self) -> str:
|
|
27597
|
+
"""Read plan.md; returns empty string if file does not exist."""
|
|
27598
|
+
try:
|
|
27599
|
+
p = self._plan_file_path()
|
|
27600
|
+
return p.read_text(encoding="utf-8") if p.exists() else ""
|
|
27601
|
+
except Exception:
|
|
27602
|
+
return ""
|
|
27603
|
+
|
|
27604
|
+
def _format_plan_file_preselection(self, proposal: dict) -> str:
|
|
27605
|
+
"""Full MD content with ALL options for model review (no char limit)."""
|
|
27606
|
+
lines = ["# Execution Plan Proposals\n"]
|
|
27607
|
+
context = str(proposal.get("context", "") or "").strip()
|
|
27608
|
+
if context:
|
|
27609
|
+
lines.append(f"## Background\n{context}\n")
|
|
27610
|
+
recommended = str(proposal.get("recommended", "") or "").strip()
|
|
27611
|
+
options = proposal.get("options", [])
|
|
27612
|
+
if not isinstance(options, list):
|
|
27613
|
+
options = []
|
|
27614
|
+
for opt in options[:PLAN_MODE_MAX_OPTIONS]:
|
|
27615
|
+
if not isinstance(opt, dict):
|
|
27616
|
+
continue
|
|
27617
|
+
opt_id = str(opt.get("id", "") or "").strip()
|
|
27618
|
+
title = str(opt.get("title", "") or "").strip()
|
|
27619
|
+
header = f"## Option {opt_id}: {title}"
|
|
27620
|
+
if opt_id == recommended:
|
|
27621
|
+
header += " [RECOMMENDED]"
|
|
27622
|
+
lines.append("---\n")
|
|
27623
|
+
lines.append(header)
|
|
27624
|
+
summary = str(opt.get("summary", "") or "").strip()
|
|
27625
|
+
if summary:
|
|
27626
|
+
lines.append(summary)
|
|
27627
|
+
steps = opt.get("steps", [])
|
|
27628
|
+
if isinstance(steps, list) and steps:
|
|
27629
|
+
lines.append("\n### Steps")
|
|
27630
|
+
for i, s in enumerate(steps):
|
|
27631
|
+
lines.append(f"{i + 1}. {s}")
|
|
27632
|
+
pros = str(opt.get("pros", "") or "").strip()
|
|
27633
|
+
if pros:
|
|
27634
|
+
lines.append(f"\n**Pros:** {pros}")
|
|
27635
|
+
cons = str(opt.get("cons", "") or "").strip()
|
|
27636
|
+
if cons:
|
|
27637
|
+
lines.append(f"**Cons:** {cons}")
|
|
27638
|
+
risk = str(opt.get("risk", "") or "").strip()
|
|
27639
|
+
if risk:
|
|
27640
|
+
lines.append(f"**Risk:** {risk}")
|
|
27641
|
+
lines.append("")
|
|
27642
|
+
lines.append("---")
|
|
27643
|
+
lines.append("> Awaiting user choice.")
|
|
27644
|
+
return "\n".join(lines)
|
|
27645
|
+
|
|
27646
|
+
def _format_plan_file_execution(self, choice_id: str) -> str:
|
|
27647
|
+
"""Render execution-phase plan.md with live step statuses from blackboard."""
|
|
27648
|
+
bb = self._ensure_blackboard()
|
|
27649
|
+
proposal = self.runtime_plan_proposal or {}
|
|
27650
|
+
chosen = next(
|
|
27651
|
+
(o for o in proposal.get("options", [])
|
|
27652
|
+
if isinstance(o, dict) and o.get("id") == choice_id),
|
|
27653
|
+
None,
|
|
27654
|
+
)
|
|
27655
|
+
title = str((chosen or {}).get("title", "") or choice_id).strip()
|
|
27656
|
+
summary = str((chosen or {}).get("summary", "") or "").strip()
|
|
27657
|
+
todos = bb.get("project_todos", [])
|
|
27658
|
+
plan_todos = [t for t in todos if t.get("category") == "plan_step"]
|
|
27659
|
+
total = len(plan_todos)
|
|
27660
|
+
completed = sum(1 for t in plan_todos if t.get("status") == "completed")
|
|
27661
|
+
current_idx = completed + 1
|
|
27662
|
+
|
|
27663
|
+
lines = [f"# Active Plan: {title}\n"]
|
|
27664
|
+
lines.append(f"> Status: EXECUTING | Step {current_idx}/{total}")
|
|
27665
|
+
lines.append(f"> Chosen: Option {choice_id}")
|
|
27666
|
+
from datetime import datetime as _dt_cls
|
|
27667
|
+
lines.append(f"> Updated: {_dt_cls.now().isoformat(timespec='seconds')}\n")
|
|
27668
|
+
if summary:
|
|
27669
|
+
lines.append(f"## Summary\n{summary}\n")
|
|
27670
|
+
lines.append("## Steps\n")
|
|
27671
|
+
for t in plan_todos:
|
|
27672
|
+
idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
27673
|
+
text = str(t.get("content", "") or "").strip()
|
|
27674
|
+
status = str(t.get("status", "pending") or "pending")
|
|
27675
|
+
if status == "completed":
|
|
27676
|
+
actor = str(t.get("completed_by", "") or "")
|
|
27677
|
+
evidence = str(t.get("evidence", "") or "")
|
|
27678
|
+
lines.append(f"- [x] Step {idx}: {text}")
|
|
27679
|
+
meta_parts = []
|
|
27680
|
+
if actor:
|
|
27681
|
+
meta_parts.append(f"Completed by: {actor}")
|
|
27682
|
+
if evidence:
|
|
27683
|
+
meta_parts.append(f"Evidence: {evidence}")
|
|
27684
|
+
if meta_parts:
|
|
27685
|
+
lines.append(f" > {' | '.join(meta_parts)}")
|
|
27686
|
+
elif status == "in_progress":
|
|
27687
|
+
lines.append(f"- [>] Step {idx}: {text} <-- CURRENT")
|
|
27688
|
+
else:
|
|
27689
|
+
lines.append(f"- [ ] Step {idx}: {text}")
|
|
27690
|
+
return "\n".join(lines) + "\n"
|
|
27691
|
+
|
|
27692
|
+
def _update_plan_file_step_status(self) -> bool:
|
|
27693
|
+
"""Re-render execution-phase plan.md from current blackboard state and write atomically."""
|
|
27694
|
+
choice_id = str(self.runtime_plan_choice or "")
|
|
27695
|
+
if not choice_id:
|
|
27696
|
+
bb = self._ensure_blackboard()
|
|
27697
|
+
plan_data = bb.get("plan", {})
|
|
27698
|
+
choice_id = str(plan_data.get("chosen", "") if isinstance(plan_data, dict) else "")
|
|
27699
|
+
if not choice_id:
|
|
27700
|
+
return False
|
|
27701
|
+
content = self._format_plan_file_execution(choice_id)
|
|
27702
|
+
return self._write_plan_file(content)
|
|
27703
|
+
|
|
27704
|
+
def _format_plan_bubble_preselection(self, proposal: dict) -> str:
|
|
27705
|
+
"""Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS). No full step listing."""
|
|
27706
|
+
lines = ["## 📋 执行方案\n"]
|
|
27707
|
+
context = str(proposal.get("context", "") or "").strip()
|
|
27708
|
+
if context:
|
|
27709
|
+
lines.append(f"**背景:** {trim(context, 300)}\n")
|
|
27710
|
+
recommended = str(proposal.get("recommended", "") or "").strip()
|
|
27711
|
+
options = proposal.get("options", [])
|
|
27712
|
+
if not isinstance(options, list):
|
|
27713
|
+
options = []
|
|
27714
|
+
for opt in options[:PLAN_MODE_MAX_OPTIONS]:
|
|
27715
|
+
if not isinstance(opt, dict):
|
|
27716
|
+
continue
|
|
27717
|
+
opt_id = str(opt.get("id", "") or "").strip()
|
|
27718
|
+
title = str(opt.get("title", "") or "").strip()
|
|
27719
|
+
is_rec = opt_id == recommended
|
|
27720
|
+
header = f"### 方案 {opt_id}: {title}"
|
|
27721
|
+
if is_rec:
|
|
27722
|
+
header += " ⭐推荐"
|
|
27723
|
+
lines.append(header)
|
|
27724
|
+
summary = str(opt.get("summary", "") or "").strip()
|
|
27725
|
+
if summary:
|
|
27726
|
+
lines.append(trim(summary, 200))
|
|
27727
|
+
steps = opt.get("steps", [])
|
|
27728
|
+
step_count = len(steps) if isinstance(steps, list) else 0
|
|
27729
|
+
risk = str(opt.get("risk", "") or "").strip()
|
|
27730
|
+
meta = f"步骤数: {step_count}"
|
|
27731
|
+
if risk:
|
|
27732
|
+
meta += f" | 风险: {risk}"
|
|
27733
|
+
lines.append(meta)
|
|
27734
|
+
lines.append("")
|
|
27735
|
+
lines.append("---")
|
|
27736
|
+
lines.append(f"完整方案详见: `{PLAN_FILE_RELATIVE_PATH}`")
|
|
27737
|
+
lines.append('请回复选择(如"方案A"、"A"、"选1"),或输入修改意见。')
|
|
27738
|
+
return trim("\n".join(lines), PLAN_BUBBLE_MAX_CHARS)
|
|
27739
|
+
|
|
27740
|
+
def _plan_file_read_instruction(self) -> str:
|
|
27741
|
+
"""Short instruction for models: read the plan file instead of embedding full plan text."""
|
|
27742
|
+
return (
|
|
27743
|
+
f"[plan-file] The approved execution plan is at `{PLAN_FILE_RELATIVE_PATH}`.\n"
|
|
27744
|
+
f"Use: read_file {PLAN_FILE_RELATIVE_PATH} to review full steps and live status.\n"
|
|
27745
|
+
"The plan file is the authoritative source for step ordering and completion status.\n"
|
|
27746
|
+
"Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing.\n"
|
|
27747
|
+
"If a step references a skill or workflow, call load_skill to load it before proceeding."
|
|
27748
|
+
)
|
|
27749
|
+
|
|
27750
|
+
# ── (legacy) _format_plan_proposal_markdown ──────────────────────
|
|
27751
|
+
|
|
27228
27752
|
def _format_plan_proposal_markdown(self, proposal: dict) -> str:
|
|
27229
27753
|
lines = ["## 📋 执行方案\n"]
|
|
27230
27754
|
context = str(proposal.get("context", "") or "").strip()
|
|
@@ -27350,16 +27874,9 @@ class SessionState:
|
|
|
27350
27874
|
)
|
|
27351
27875
|
if not chosen:
|
|
27352
27876
|
return
|
|
27353
|
-
|
|
27354
|
-
|
|
27355
|
-
|
|
27356
|
-
f"## {chosen.get('title', '')}\n"
|
|
27357
|
-
f"{chosen.get('summary', '')}\n\n"
|
|
27358
|
-
f"### Steps:\n{steps_text}\n\n"
|
|
27359
|
-
f"Follow these steps. Use tools to implement each step concretely.\n"
|
|
27360
|
-
f"If a step references a skill or workflow you don't fully understand, "
|
|
27361
|
-
f"call load_skill to load it and read its instructions before proceeding."
|
|
27362
|
-
)
|
|
27877
|
+
# Write execution-phase plan file (detailed, for model read_file)
|
|
27878
|
+
# Must happen BEFORE blackboard todos are created so the file reflects initial state
|
|
27879
|
+
plan_msg = self._plan_file_read_instruction()
|
|
27363
27880
|
self.messages.append({
|
|
27364
27881
|
"role": "system",
|
|
27365
27882
|
"content": plan_msg,
|
|
@@ -27377,6 +27894,9 @@ class SessionState:
|
|
|
27377
27894
|
bb["plan"] = {"phase": "executing", "chosen": choice_id, "steps": chosen.get("steps", [])}
|
|
27378
27895
|
self.blackboard = bb
|
|
27379
27896
|
self._blackboard_history("manager", f"plan approved: option {choice_id} — {chosen.get('title', '')}")
|
|
27897
|
+
# Lock complexity/level floor to prevent manager downgrade during plan execution
|
|
27898
|
+
self.runtime_complexity_floor = str(self.runtime_task_complexity or "complex")
|
|
27899
|
+
self.runtime_task_level_floor = int(self.runtime_task_level or 4)
|
|
27380
27900
|
# Auto-create todos from plan steps → write into bb["project_todos"]
|
|
27381
27901
|
steps = chosen.get("steps", [])
|
|
27382
27902
|
if steps and isinstance(steps, list):
|
|
@@ -27397,7 +27917,7 @@ class SessionState:
|
|
|
27397
27917
|
"evidence": "",
|
|
27398
27918
|
})
|
|
27399
27919
|
if plan_todos:
|
|
27400
|
-
bb["project_todos"] = plan_todos[:
|
|
27920
|
+
bb["project_todos"] = plan_todos[:40]
|
|
27401
27921
|
bb["plan_step_cursor"] = 0
|
|
27402
27922
|
bb["plan_step_total"] = len(plan_todos)
|
|
27403
27923
|
self.blackboard = bb
|
|
@@ -27412,12 +27932,13 @@ class SessionState:
|
|
|
27412
27932
|
"status": t["status"],
|
|
27413
27933
|
"activeForm": f"Working on: {t['content']}" if t["status"] == "in_progress" else f"Pending: {t['content']}",
|
|
27414
27934
|
}
|
|
27415
|
-
for t in plan_todos[:
|
|
27935
|
+
for t in plan_todos[:40]
|
|
27416
27936
|
])
|
|
27417
27937
|
except Exception:
|
|
27418
27938
|
pass
|
|
27939
|
+
# Write execution-phase plan file (now with todos populated)
|
|
27419
27940
|
try:
|
|
27420
|
-
|
|
27941
|
+
self._write_plan_file(self._format_plan_file_execution(choice_id))
|
|
27421
27942
|
except Exception:
|
|
27422
27943
|
pass
|
|
27423
27944
|
# Pre-load skills explicitly mentioned in plan steps
|
|
@@ -30507,6 +31028,9 @@ h3{font-size:.96rem;margin:10px 0 6px}
|
|
|
30507
31028
|
.todo-item,.task-item{border:1px solid #e4ebf4;border-left-width:4px;border-radius:10px;padding:8px 10px;background:#fcfdff}
|
|
30508
31029
|
.todo-item.st-pending,.task-item.st-pending{border-left-color:#7b8798}
|
|
30509
31030
|
.todo-item.st-in_progress,.task-item.st-in_progress{border-left-color:#1f6feb;background:#eef5ff}
|
|
31031
|
+
.todo-group-label{font-size:.72rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:6px 0 2px 2px}
|
|
31032
|
+
.todo-subtask{margin-left:16px;border-left-width:3px;border-radius:8px;padding:6px 10px;font-size:.9em}
|
|
31033
|
+
.todo-subtask::before{content:"↳ ";color:var(--muted);font-size:.85em}
|
|
30510
31034
|
.todo-item.st-completed,.task-item.st-completed{border-left-color:#13b8a6;background:#edfcf7}
|
|
30511
31035
|
.todo-item.st-blocked,.task-item.st-blocked{border-left-color:#b96b00;background:#fff6ea}
|
|
30512
31036
|
.todo-item.st-deleted,.task-item.st-deleted{border-left-color:#a0a6b0;background:#f7f8fa}
|
|
@@ -30665,7 +31189,8 @@ const I18N={
|
|
|
30665
31189
|
llm_fill_config:'Fill LLM Config',llm_provider:'Provider',llm_confirm:'Confirm',llm_import_config:'Import config',
|
|
30666
31190
|
llm_thinking_stream:'Thinking Stream',llm_enabled:'Enabled',llm_disabled:'Disabled',
|
|
30667
31191
|
llm_model:'Model',llm_scan:'Scan',llm_scan_hint:'Click Scan to detect models from Ollama',llm_scan_first:'Scan models first',
|
|
30668
|
-
llm_scanning:'Scanning...',llm_scan_found:'Found {n} model(s)',llm_scan_empty:'No models found',llm_scan_error:'Scan failed'
|
|
31192
|
+
llm_scanning:'Scanning...',llm_scan_found:'Found {n} model(s)',llm_scan_empty:'No models found',llm_scan_error:'Scan failed',
|
|
31193
|
+
todo_plan_steps:'Plan Steps',todo_subtasks:'Subtasks'
|
|
30669
31194
|
},
|
|
30670
31195
|
'zh-CN':{
|
|
30671
31196
|
app_title:'Clouds Coder',app_subtitle:'WebUI 驱动的会话式集成编程 Agent 平台',powered_by:'Powered By Fona',
|
|
@@ -30700,7 +31225,8 @@ const I18N={
|
|
|
30700
31225
|
llm_fill_config:'填写 LLM 配置',llm_provider:'供应商',llm_confirm:'确认',llm_import_config:'导入配置',
|
|
30701
31226
|
llm_thinking_stream:'思维流',llm_enabled:'启用',llm_disabled:'禁用',
|
|
30702
31227
|
llm_model:'模型',llm_scan:'扫描',llm_scan_hint:'点击扫描检测 Ollama 可用模型',llm_scan_first:'请先扫描模型',
|
|
30703
|
-
llm_scanning:'扫描中...',llm_scan_found:'发现 {n} 个模型',llm_scan_empty:'未发现模型',llm_scan_error:'扫描失败'
|
|
31228
|
+
llm_scanning:'扫描中...',llm_scan_found:'发现 {n} 个模型',llm_scan_empty:'未发现模型',llm_scan_error:'扫描失败',
|
|
31229
|
+
todo_plan_steps:'计划步骤',todo_subtasks:'子任务'
|
|
30704
31230
|
},
|
|
30705
31231
|
'zh-TW':{
|
|
30706
31232
|
app_title:'Clouds Coder',app_subtitle:'WebUI 驅動的會話式整合程式 Agent 平台',powered_by:'Powered By Fona',
|
|
@@ -30735,7 +31261,8 @@ const I18N={
|
|
|
30735
31261
|
llm_fill_config:'填寫 LLM 設定',llm_provider:'供應商',llm_confirm:'確認',llm_import_config:'匯入設定',
|
|
30736
31262
|
llm_thinking_stream:'思維流',llm_enabled:'啟用',llm_disabled:'停用',
|
|
30737
31263
|
llm_model:'模型',llm_scan:'掃描',llm_scan_hint:'點擊掃描偵測 Ollama 可用模型',llm_scan_first:'請先掃描模型',
|
|
30738
|
-
llm_scanning:'掃描中...',llm_scan_found:'發現 {n} 個模型',llm_scan_empty:'未發現模型',llm_scan_error:'掃描失敗'
|
|
31264
|
+
llm_scanning:'掃描中...',llm_scan_found:'發現 {n} 個模型',llm_scan_empty:'未發現模型',llm_scan_error:'掃描失敗',
|
|
31265
|
+
todo_plan_steps:'計劃步驟',todo_subtasks:'子任務'
|
|
30739
31266
|
},
|
|
30740
31267
|
'ja':{
|
|
30741
31268
|
app_title:'Clouds Coder',app_subtitle:'WebUI 駆動の対話型コーディング Agent プラットフォーム',powered_by:'Powered By Fona',
|
|
@@ -30770,7 +31297,8 @@ const I18N={
|
|
|
30770
31297
|
llm_fill_config:'LLM設定入力',llm_provider:'プロバイダー',llm_confirm:'確認',llm_import_config:'設定をインポート',
|
|
30771
31298
|
llm_thinking_stream:'シンキングストリーム',llm_enabled:'有効',llm_disabled:'無効',
|
|
30772
31299
|
llm_model:'モデル',llm_scan:'スキャン',llm_scan_hint:'スキャンをクリックしてOllamaモデルを検出',llm_scan_first:'先にモデルをスキャン',
|
|
30773
|
-
llm_scanning:'スキャン中...',llm_scan_found:'{n}個のモデルを検出',llm_scan_empty:'モデルが見つかりません',llm_scan_error:'スキャン失敗'
|
|
31300
|
+
llm_scanning:'スキャン中...',llm_scan_found:'{n}個のモデルを検出',llm_scan_empty:'モデルが見つかりません',llm_scan_error:'スキャン失敗',
|
|
31301
|
+
todo_plan_steps:'計画ステップ',todo_subtasks:'サブタスク'
|
|
30774
31302
|
}
|
|
30775
31303
|
};
|
|
30776
31304
|
function currentLang(){const fromSnap=String(S.snap?.ui_language||'').trim();if(fromSnap&&I18N[fromSnap])return fromSnap;const fromCfg=String(S.config?.language||'').trim();if(fromCfg&&I18N[fromCfg])return fromCfg;return 'zh-CN'}
|
|
@@ -33081,7 +33609,54 @@ function statusClass(status){return `st-${normalizeStatus(status)}`}
|
|
|
33081
33609
|
function statusLabel(status){const s=normalizeStatus(status);if(s==='in_progress')return t('status_in_progress');if(s==='completed')return t('status_completed');if(s==='blocked')return t('status_blocked');if(s==='deleted')return t('status_deleted');return t('status_pending')}
|
|
33082
33610
|
function cleanWorkText(text,status=''){let s=String(text??'').replace(/\\s+/g,' ').trim();if(!s)return '';s=s.replace(/^\\[[ x>\\-]\\]\\s*/i,'');s=s.replace(/^(pending|in[_\\-\\s]?progress|completed|done|blocked)\\s*[·:\\-\\]]\\s*/i,'');if(status){const st=String(status).replace('_','[_\\\\-\\\\s]?');s=s.replace(new RegExp(`\\\\s*[—-]\\\\s*${st}\\\\s*$`,'i'),'')}s=s.replace(/\\s*[—-]\\s*(pending|in[_\\-\\s]?progress|completed|done|blocked)\\s*$/i,'');return s.trim()||String(text??'').trim()}
|
|
33083
33611
|
function formatTs(ts){const v=Number(ts||0);if(!v)return '';try{return new Date(v*1000).toLocaleString()}catch(_){return ''}}
|
|
33084
|
-
function renderTodoBoard(items){
|
|
33612
|
+
function renderTodoBoard(items){
|
|
33613
|
+
const todos=Array.isArray(items)?items:[];
|
|
33614
|
+
if(!todos.length)return `<div class="mono">${esc(t('no_todos'))}</div>`;
|
|
33615
|
+
const done=todos.filter(x=>normalizeStatus(x?.status)==='completed').length;
|
|
33616
|
+
const open=todos.length-done;
|
|
33617
|
+
function todoCard(item,idx,extraClass=''){
|
|
33618
|
+
const status=normalizeStatus(item?.status);
|
|
33619
|
+
const content=cleanWorkText(item?.content,status)||'(empty todo)';
|
|
33620
|
+
const active=String(item?.activeForm||'').trim();
|
|
33621
|
+
const meta=status==='in_progress'&&active?`<div class="todo-meta">${esc(cleanWorkText(active,status))}</div>`:'';
|
|
33622
|
+
return `<div class="todo-item ${statusClass(status)}${extraClass?(' '+extraClass):''}"><div class="todo-head"><span class="status-badge ${statusClass(status)}">${esc(statusLabel(status))}</span><span class="mono todo-index">#${idx+1}</span></div><div class="todo-content">${esc(content)}</div>${meta}</div>`;
|
|
33623
|
+
}
|
|
33624
|
+
// Split into plan steps (bb:proj:) and worker subtasks
|
|
33625
|
+
const planSteps=todos.filter(x=>String(x?.key||'').startsWith('bb:proj:'));
|
|
33626
|
+
const workerTodos=todos.filter(x=>!String(x?.key||'').startsWith('bb:proj:'));
|
|
33627
|
+
let html='';
|
|
33628
|
+
if(planSteps.length&&workerTodos.length){
|
|
33629
|
+
// Build parent_step_id index: map step key suffix to its subtasks
|
|
33630
|
+
const stepIdFromKey=(key)=>{const k=String(key||'');return k.startsWith('bb:proj:')?k.slice(8):''};
|
|
33631
|
+
const subtasksByStep={};
|
|
33632
|
+
const unlinked=[];
|
|
33633
|
+
workerTodos.forEach(sub=>{
|
|
33634
|
+
const pid=String(sub?.parent_step_id||'').trim();
|
|
33635
|
+
if(pid){(subtasksByStep[pid]=subtasksByStep[pid]||[]).push(sub)}
|
|
33636
|
+
else{unlinked.push(sub)}
|
|
33637
|
+
});
|
|
33638
|
+
// Find active plan step for unlinked subtasks fallback
|
|
33639
|
+
const activeStepIdx=planSteps.findIndex(x=>normalizeStatus(x?.status)==='in_progress');
|
|
33640
|
+
html+=`<div class="todo-group-label">${esc(t('todo_plan_steps'))}</div><div class="todo-list">`;
|
|
33641
|
+
planSteps.forEach((step,i)=>{
|
|
33642
|
+
html+=todoCard(step,i);
|
|
33643
|
+
const sid=stepIdFromKey(step?.key);
|
|
33644
|
+
// Show subtasks linked to this step via parent_step_id
|
|
33645
|
+
const linked=sid?subtasksByStep[sid]||[]:[];
|
|
33646
|
+
// Also attach unlinked subtasks under the active plan step (backward compat)
|
|
33647
|
+
const subs=i===activeStepIdx?linked.concat(unlinked):linked;
|
|
33648
|
+
if(subs.length){
|
|
33649
|
+
html+=`<div class="todo-group-label" style="margin-left:16px">${esc(t('todo_subtasks'))}</div>`;
|
|
33650
|
+
subs.forEach((sub,j)=>{html+=todoCard(sub,j,'todo-subtask')});
|
|
33651
|
+
}
|
|
33652
|
+
});
|
|
33653
|
+
html+=`</div>`;
|
|
33654
|
+
} else {
|
|
33655
|
+
// No grouping needed — flat list
|
|
33656
|
+
html+=`<div class="todo-list">${todos.map((x,i)=>todoCard(x,i)).join('')}</div>`;
|
|
33657
|
+
}
|
|
33658
|
+
return `<div class="board-summary"><span>${esc(open)} ${esc(t('open'))}</span><span>${esc(done)}/${esc(todos.length)} ${esc(t('completed'))}</span></div>${html}`;
|
|
33659
|
+
}
|
|
33085
33660
|
function renderTaskBoard(items){const tasks=Array.isArray(items)?items:[];if(!tasks.length)return `<div class=\"mono\">${esc(t('no_tasks'))}</div>`;const completed=tasks.filter(row=>normalizeStatus(row?.status,'pending')==='completed').length;const blocked=tasks.filter(row=>normalizeStatus(row?.status,'pending')==='blocked').length;const cards=tasks.map(row=>{const status=normalizeStatus(row?.status,'pending');const id=Number(row?.id||0)||'-';const subject=cleanWorkText(row?.subject,status)||'(empty task)';const owner=String(row?.owner||'').trim();const blockedBy=Array.isArray(row?.blockedBy)&&row.blockedBy.length?`blocked_by=${row.blockedBy.map(x=>`#${x}`).join(', ')}`:'';const blocks=Array.isArray(row?.blocks)&&row.blocks.length?`blocks=${row.blocks.map(x=>`#${x}`).join(', ')}`:'';const timeTxt=formatTs(row?.updated_at||row?.created_at);const meta=[owner?`owner=@${owner}`:t('owner_unassigned'),blockedBy,blocks,timeTxt].filter(Boolean).join(' · ');return `<div class=\"task-item ${statusClass(status)}\"><div class=\"task-head\"><span class=\"mono task-id\">#${esc(id)}</span><span class=\"status-badge ${statusClass(status)}\">${esc(statusLabel(status))}</span></div><div class=\"task-subject\">${esc(subject)}</div><div class=\"task-meta\">${esc(meta)}</div></div>`}).join('');return `<div class=\"board-summary\"><span>${esc(tasks.length-completed)} ${esc(t('open'))}</span><span>${esc(completed)} ${esc(t('completed'))} · ${esc(blocked)} ${esc(t('blocked'))}</span></div><div class=\"task-list\">${cards}</div>`}
|
|
33086
33661
|
function ensureFileExplorerState(sessionId){const sid=String(sessionId||S.activeId||'').trim();if(!sid)return null;if(!S.fileExplorerBySession)S.fileExplorerBySession={};if(!S.fileExplorerBySession[sid]||typeof S.fileExplorerBySession[sid]!=='object'){S.fileExplorerBySession[sid]={tree:null,root:'',nodeCount:0,truncated:false,maxNodes:0,fetchedAt:0,inflight:false,selected:'',expanded:{'':true}}}const st=S.fileExplorerBySession[sid];if(!st.expanded||typeof st.expanded!=='object')st.expanded={'':true};st.expanded['']=true;return st}
|
|
33087
33662
|
function _fePath(sessionId){const sid=encodeURIComponent(String(sessionId||'').trim());return `/api/sessions/${sid}/files-tree`}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clouds-coder"
|
|
7
|
-
version = "2026.3.
|
|
7
|
+
version = "2026.3.28"
|
|
8
8
|
description = "Clouds Coder: local-first Cloud CLI coder runtime with Web UI and Skills Studio."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clouds_coder-2026.3.27 → clouds_coder-2026.3.28}/clouds_coder.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|