clouds-coder 2026.3.25__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.25 → clouds_coder-2026.3.28}/Clouds_Coder.py +1521 -210
- {clouds_coder-2026.3.25/clouds_coder.egg-info → clouds_coder-2026.3.28}/PKG-INFO +25 -3
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/README.md +24 -2
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28/clouds_coder.egg-info}/PKG-INFO +25 -3
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/pyproject.toml +1 -1
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/LICENSE +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/setup.cfg +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.28}/tests/test_smoke.py +0 -0
|
@@ -27,6 +27,7 @@ import shlex
|
|
|
27
27
|
import socket
|
|
28
28
|
import subprocess
|
|
29
29
|
import sys
|
|
30
|
+
import tarfile
|
|
30
31
|
import threading
|
|
31
32
|
import time
|
|
32
33
|
import traceback
|
|
@@ -103,7 +104,7 @@ CODE_LIBRARY_DIRNAME = "Code_Library"
|
|
|
103
104
|
CODE_ADMIN_PORT_OFFSET = 3
|
|
104
105
|
RAG_CHUNK_CHARS = 1200
|
|
105
106
|
RAG_CHUNK_OVERLAP = 180
|
|
106
|
-
RAG_MAX_CHUNKS_PER_DOC =
|
|
107
|
+
RAG_MAX_CHUNKS_PER_DOC = 500
|
|
107
108
|
CODE_CHUNK_CHARS = 1800
|
|
108
109
|
CODE_CHUNK_OVERLAP = 120
|
|
109
110
|
CODE_MAX_CHUNKS_PER_DOC = 260
|
|
@@ -152,7 +153,7 @@ MIN_AGENT_ROUNDS = 8
|
|
|
152
153
|
MAX_AGENT_ROUNDS_CAP = 400
|
|
153
154
|
REPEATED_TOOL_LOOP_THRESHOLD = 2
|
|
154
155
|
BASH_READ_LOOP_THRESHOLD = 3
|
|
155
|
-
HARD_BREAK_TOOL_ERROR_THRESHOLD =
|
|
156
|
+
HARD_BREAK_TOOL_ERROR_THRESHOLD = 20
|
|
156
157
|
HARD_BREAK_RECOVERY_ROUND_THRESHOLD = 3
|
|
157
158
|
FUSED_FAULT_BREAK_THRESHOLD = 3
|
|
158
159
|
STALL_SEVERITY_ESCALATION_THRESHOLD = 5
|
|
@@ -454,7 +455,7 @@ PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
|
|
|
454
455
|
TASK_PHASES = ("research", "design", "implement", "test", "review", "deploy")
|
|
455
456
|
TASK_PHASE_ROUTING = {
|
|
456
457
|
"research": "explorer",
|
|
457
|
-
"design": "
|
|
458
|
+
"design": "developer",
|
|
458
459
|
"implement": "developer",
|
|
459
460
|
"test": "developer",
|
|
460
461
|
"review": "reviewer",
|
|
@@ -479,6 +480,8 @@ EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before force
|
|
|
479
480
|
DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same file before forced strategy change
|
|
480
481
|
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 4096
|
|
481
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)
|
|
482
485
|
PLAN_MODE_RESEARCH_TOOL_ALLOWLIST = {
|
|
483
486
|
"bash", "read_file", "context_recall", "task_get", "task_list",
|
|
484
487
|
"check_background", "read_from_blackboard", "write_to_blackboard",
|
|
@@ -974,11 +977,214 @@ OFFLINE_JS_LIB_CATALOG: list[dict[str, object]] = [
|
|
|
974
977
|
],
|
|
975
978
|
"match_tokens": ["cdn.tailwindcss.com", "@tailwindcss/browser", "tailwindcss"],
|
|
976
979
|
},
|
|
980
|
+
{
|
|
981
|
+
"id": "mathjax",
|
|
982
|
+
"filename": "tex-mml-chtml.js",
|
|
983
|
+
"relative_path": "mathjax/es5/tex-mml-chtml.js",
|
|
984
|
+
"package_urls": [
|
|
985
|
+
"https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz",
|
|
986
|
+
],
|
|
987
|
+
"package_install_dir": "mathjax",
|
|
988
|
+
"package_required_paths": [
|
|
989
|
+
"package.json",
|
|
990
|
+
"es5/core.js",
|
|
991
|
+
"es5/loader.js",
|
|
992
|
+
"es5/startup.js",
|
|
993
|
+
"es5/tex-mml-chtml.js",
|
|
994
|
+
],
|
|
995
|
+
"match_tokens": ["mathjax", "tex-mml-chtml.js", "/mathjax@"],
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
"id": "katex",
|
|
999
|
+
"filename": "katex.min.js",
|
|
1000
|
+
"relative_path": "katex/dist/katex.min.js",
|
|
1001
|
+
"package_urls": [
|
|
1002
|
+
"https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
|
|
1003
|
+
],
|
|
1004
|
+
"package_install_dir": "katex",
|
|
1005
|
+
"package_required_paths": [
|
|
1006
|
+
"package.json",
|
|
1007
|
+
"dist/katex.min.js",
|
|
1008
|
+
"dist/katex.min.css",
|
|
1009
|
+
"dist/contrib/auto-render.min.js",
|
|
1010
|
+
],
|
|
1011
|
+
"match_tokens": ["katex", "katex.min.js", "/katex@"],
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
"id": "katex_auto_render",
|
|
1015
|
+
"filename": "auto-render.min.js",
|
|
1016
|
+
"relative_path": "katex/dist/contrib/auto-render.min.js",
|
|
1017
|
+
"match_tokens": ["auto-render.min.js", "katex/contrib/auto-render", "katex-auto-render"],
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
"id": "html2canvas",
|
|
1021
|
+
"filename": "html2canvas.min.js",
|
|
1022
|
+
"urls": [
|
|
1023
|
+
"https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js",
|
|
1024
|
+
"https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js",
|
|
1025
|
+
],
|
|
1026
|
+
"match_tokens": ["html2canvas", "html2canvas.min.js"],
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
"id": "jspdf",
|
|
1030
|
+
"filename": "jspdf.umd.min.js",
|
|
1031
|
+
"urls": [
|
|
1032
|
+
"https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js",
|
|
1033
|
+
"https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js",
|
|
1034
|
+
],
|
|
1035
|
+
"match_tokens": ["jspdf", "jspdf.umd.min.js"],
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
"id": "xlsx",
|
|
1039
|
+
"filename": "xlsx.full.min.js",
|
|
1040
|
+
"urls": [
|
|
1041
|
+
"https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js",
|
|
1042
|
+
"https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js",
|
|
1043
|
+
],
|
|
1044
|
+
"match_tokens": ["xlsx", "xlsx.full.min.js", "sheetjs"],
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"id": "jszip",
|
|
1048
|
+
"filename": "jszip.min.js",
|
|
1049
|
+
"relative_path": "node_modules/jszip/dist/jszip.min.js",
|
|
1050
|
+
"package_urls": [
|
|
1051
|
+
"https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
|
1052
|
+
],
|
|
1053
|
+
"package_install_dir": "node_modules/jszip",
|
|
1054
|
+
"package_required_paths": [
|
|
1055
|
+
"package.json",
|
|
1056
|
+
"dist/jszip.min.js",
|
|
1057
|
+
],
|
|
1058
|
+
"package_postprocess": "jszip-main-to-dist",
|
|
1059
|
+
"match_tokens": ["jszip", "jszip.min.js"],
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
"id": "pptxgenjs",
|
|
1063
|
+
"filename": "pptxgen.bundle.js",
|
|
1064
|
+
"relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
|
|
1065
|
+
"package_urls": [
|
|
1066
|
+
"https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
|
|
1067
|
+
],
|
|
1068
|
+
"package_install_dir": "pptxgenjs",
|
|
1069
|
+
"package_required_paths": [
|
|
1070
|
+
"package.json",
|
|
1071
|
+
"dist/pptxgen.bundle.js",
|
|
1072
|
+
"dist/pptxgen.cjs.js",
|
|
1073
|
+
"dist/pptxgen.es.js",
|
|
1074
|
+
"dist/pptxgen.min.js",
|
|
1075
|
+
],
|
|
1076
|
+
"match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.cjs.js", "pptxgen.es.js", "pptxgen.min.js", "jszip.min.js"],
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
"id": "pptxgenjs_bundle",
|
|
1080
|
+
"filename": "pptxgen.bundle.js",
|
|
1081
|
+
"relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
|
|
1082
|
+
"match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.bundle"],
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
"id": "pptxgenjs_cjs",
|
|
1086
|
+
"filename": "pptxgen.cjs.js",
|
|
1087
|
+
"relative_path": "pptxgenjs/dist/pptxgen.cjs.js",
|
|
1088
|
+
"match_tokens": ["pptxgenjs", "pptxgen.cjs.js", "pptxgen.cjs"],
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
"id": "pptxgenjs_es",
|
|
1092
|
+
"filename": "pptxgen.es.js",
|
|
1093
|
+
"relative_path": "pptxgenjs/dist/pptxgen.es.js",
|
|
1094
|
+
"match_tokens": ["pptxgenjs", "pptxgen.es.js", "pptxgen.es"],
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
"id": "pptxgenjs_min",
|
|
1098
|
+
"filename": "pptxgen.min.js",
|
|
1099
|
+
"relative_path": "pptxgenjs/dist/pptxgen.min.js",
|
|
1100
|
+
"match_tokens": ["pptxgenjs", "pptxgen.min.js", "pptxgen.min"],
|
|
1101
|
+
},
|
|
977
1102
|
]
|
|
978
1103
|
OFFLINE_JS_LIB_INDEX_FILE = "index.json"
|
|
979
1104
|
OFFLINE_JS_LIB_README_FILE = "README.md"
|
|
980
1105
|
|
|
981
1106
|
|
|
1107
|
+
def _normalize_js_lib_asset_ref(value: str) -> str:
|
|
1108
|
+
raw = str(value or "").replace("\\", "/").strip()
|
|
1109
|
+
if not raw:
|
|
1110
|
+
return ""
|
|
1111
|
+
raw = raw.lstrip("/")
|
|
1112
|
+
pure = PurePosixPath(raw)
|
|
1113
|
+
parts: list[str] = []
|
|
1114
|
+
for part in pure.parts:
|
|
1115
|
+
if part in {"", "."}:
|
|
1116
|
+
continue
|
|
1117
|
+
if part == "..":
|
|
1118
|
+
return ""
|
|
1119
|
+
parts.append(part)
|
|
1120
|
+
return "/".join(parts)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _resolve_js_lib_asset_path(js_root: Path, asset_ref: str) -> Path | None:
|
|
1124
|
+
rel = _normalize_js_lib_asset_ref(asset_ref)
|
|
1125
|
+
if not rel:
|
|
1126
|
+
return None
|
|
1127
|
+
exact = (js_root / rel).resolve()
|
|
1128
|
+
try:
|
|
1129
|
+
if exact.is_file() and exact.is_relative_to(js_root):
|
|
1130
|
+
return exact
|
|
1131
|
+
except Exception:
|
|
1132
|
+
pass
|
|
1133
|
+
basename = _safe_js_filename(Path(rel).name, "lib.js")
|
|
1134
|
+
candidates: list[Path] = []
|
|
1135
|
+
try:
|
|
1136
|
+
for fp in js_root.rglob(basename):
|
|
1137
|
+
try:
|
|
1138
|
+
resolved = fp.resolve()
|
|
1139
|
+
except Exception:
|
|
1140
|
+
continue
|
|
1141
|
+
if resolved.is_file():
|
|
1142
|
+
try:
|
|
1143
|
+
if resolved.is_relative_to(js_root):
|
|
1144
|
+
candidates.append(resolved)
|
|
1145
|
+
except Exception:
|
|
1146
|
+
continue
|
|
1147
|
+
except Exception:
|
|
1148
|
+
return None
|
|
1149
|
+
if not candidates:
|
|
1150
|
+
return None
|
|
1151
|
+
candidates.sort(key=lambda p: (len(p.relative_to(js_root).parts), p.relative_to(js_root).as_posix()))
|
|
1152
|
+
return candidates[0]
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _discover_extra_js_lib_files(js_root: Path, known_relative_paths: set[str]) -> list[dict]:
|
|
1156
|
+
rows: list[dict] = []
|
|
1157
|
+
if not js_root.exists():
|
|
1158
|
+
return rows
|
|
1159
|
+
seen: set[str] = set()
|
|
1160
|
+
for fp in sorted(js_root.rglob("*")):
|
|
1161
|
+
if not fp.is_file() or fp.name in {".DS_Store", OFFLINE_JS_LIB_INDEX_FILE, OFFLINE_JS_LIB_README_FILE}:
|
|
1162
|
+
continue
|
|
1163
|
+
if fp.suffix.lower() not in {".js", ".mjs", ".cjs"}:
|
|
1164
|
+
continue
|
|
1165
|
+
rel = fp.relative_to(js_root).as_posix()
|
|
1166
|
+
if rel in known_relative_paths or rel in seen:
|
|
1167
|
+
continue
|
|
1168
|
+
seen.add(rel)
|
|
1169
|
+
stem = fp.stem.replace(".min", "").replace(".umd", "").replace(".bundle", "")
|
|
1170
|
+
rows.append(
|
|
1171
|
+
{
|
|
1172
|
+
"id": f"local:{stem or fp.name}",
|
|
1173
|
+
"filename": fp.name,
|
|
1174
|
+
"relative_path": rel,
|
|
1175
|
+
"available": True,
|
|
1176
|
+
"size": int(fp.stat().st_size),
|
|
1177
|
+
"sha256": _sha256_file(fp),
|
|
1178
|
+
"source": "existing-local",
|
|
1179
|
+
"error": "",
|
|
1180
|
+
"match_tokens": [fp.name.lower(), stem.lower(), rel.lower()],
|
|
1181
|
+
"urls": [],
|
|
1182
|
+
"catalog": False,
|
|
1183
|
+
}
|
|
1184
|
+
)
|
|
1185
|
+
return rows
|
|
1186
|
+
|
|
1187
|
+
|
|
982
1188
|
def normalize_ui_language(raw: str | None) -> str:
|
|
983
1189
|
key = str(raw or "").strip()
|
|
984
1190
|
if key in UI_LANGUAGE_LABELS:
|
|
@@ -1105,6 +1311,7 @@ def _detect_os_shell_instruction() -> str:
|
|
|
1105
1311
|
"Package manager is 'brew'. "
|
|
1106
1312
|
"Environment variables WORKSPACE_ROOT, SESSION_ROOT, and SKILLS_ROOT are available. "
|
|
1107
1313
|
"Virtual aliases '/workspace/...' and '/skills/...' are supported in shell commands and rewritten to real paths before execution. "
|
|
1314
|
+
"Virtual alias '/js_lib/...' maps to the offline JS libraries root ($JS_LIB_ROOT) and is also supported in file tools. "
|
|
1108
1315
|
"Do NOT assume Linux-specific paths like /proc or /etc/os-release exist. "
|
|
1109
1316
|
"IMPORTANT: The workspace path contains spaces. Always use relative paths "
|
|
1110
1317
|
"(e.g., 'ls uploaded/' not 'ls /full/absolute/path/uploaded/'). "
|
|
@@ -1253,6 +1460,28 @@ def extract_ui_style_setting(raw: object) -> str | None:
|
|
|
1253
1460
|
return None
|
|
1254
1461
|
|
|
1255
1462
|
|
|
1463
|
+
def extract_js_lib_download_setting(raw: object) -> bool | None:
|
|
1464
|
+
"""Read download_js_lib flag from config dict.
|
|
1465
|
+
Keys accepted: download_js_lib / js_lib_download / enable_js_lib_download
|
|
1466
|
+
Sections searched: top-level, then 'startup' / 'offline' / 'web_ui'.
|
|
1467
|
+
Returns True/False, or None if key absent (caller uses default=True).
|
|
1468
|
+
"""
|
|
1469
|
+
if not isinstance(raw, dict):
|
|
1470
|
+
return None
|
|
1471
|
+
keys = ("download_js_lib", "js_lib_download", "enable_js_lib_download", "offline_js_download")
|
|
1472
|
+
for key in keys:
|
|
1473
|
+
if key in raw:
|
|
1474
|
+
return _to_bool_like(raw.get(key), default=True)
|
|
1475
|
+
for section_key in ("startup", "offline", "web_ui", "ui"):
|
|
1476
|
+
section = raw.get(section_key)
|
|
1477
|
+
if not isinstance(section, dict):
|
|
1478
|
+
continue
|
|
1479
|
+
for key in keys:
|
|
1480
|
+
if key in section:
|
|
1481
|
+
return _to_bool_like(section.get(key), default=True)
|
|
1482
|
+
return None
|
|
1483
|
+
|
|
1484
|
+
|
|
1256
1485
|
def default_multimodal_capabilities() -> dict[str, bool]:
|
|
1257
1486
|
return {
|
|
1258
1487
|
"input_image": False,
|
|
@@ -1618,20 +1847,194 @@ def _download_http_bytes(url: str, timeout: float = 25.0) -> tuple[bytes, str]:
|
|
|
1618
1847
|
def offline_js_lib_root(workdir: Path = WORKDIR) -> Path:
|
|
1619
1848
|
return (workdir / "js_lib").resolve()
|
|
1620
1849
|
|
|
1850
|
+
def _offline_js_entry_relative_path(entry: dict[str, object], fallback_name: str) -> str:
|
|
1851
|
+
rel = _normalize_js_lib_asset_ref(str(entry.get("relative_path", "") or ""))
|
|
1852
|
+
if rel:
|
|
1853
|
+
return rel
|
|
1854
|
+
return _safe_js_filename(fallback_name, fallback_name)
|
|
1855
|
+
|
|
1856
|
+
def _archive_member_relative_path(name: str) -> str:
|
|
1857
|
+
raw = _normalize_js_lib_asset_ref(name)
|
|
1858
|
+
if not raw:
|
|
1859
|
+
return ""
|
|
1860
|
+
parts = [part for part in PurePosixPath(raw).parts if part not in {"", "."}]
|
|
1861
|
+
while parts and parts[0].lower() == "package":
|
|
1862
|
+
parts = parts[1:]
|
|
1863
|
+
if not parts:
|
|
1864
|
+
return ""
|
|
1865
|
+
return "/".join(parts)
|
|
1866
|
+
|
|
1867
|
+
def _path_size_bytes(target: Path) -> int:
|
|
1868
|
+
try:
|
|
1869
|
+
if target.is_file():
|
|
1870
|
+
return int(target.stat().st_size)
|
|
1871
|
+
if not target.exists():
|
|
1872
|
+
return 0
|
|
1873
|
+
total = 0
|
|
1874
|
+
for fp in target.rglob("*"):
|
|
1875
|
+
try:
|
|
1876
|
+
if fp.is_file():
|
|
1877
|
+
total += int(fp.stat().st_size)
|
|
1878
|
+
except Exception:
|
|
1879
|
+
continue
|
|
1880
|
+
return total
|
|
1881
|
+
except Exception:
|
|
1882
|
+
return 0
|
|
1883
|
+
|
|
1884
|
+
def _extract_archive_to_dir(raw: bytes, install_root: Path) -> list[str]:
|
|
1885
|
+
install_root.mkdir(parents=True, exist_ok=True)
|
|
1886
|
+
install_root_resolved = install_root.resolve()
|
|
1887
|
+
written: list[str] = []
|
|
1888
|
+
|
|
1889
|
+
def _write_bytes(rel: str, data: bytes):
|
|
1890
|
+
target = (install_root / rel).resolve()
|
|
1891
|
+
if not target.is_relative_to(install_root_resolved):
|
|
1892
|
+
raise ValueError(f"archive member escapes target dir: {rel}")
|
|
1893
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1894
|
+
target.write_bytes(data)
|
|
1895
|
+
written.append(rel)
|
|
1896
|
+
|
|
1897
|
+
bio = io.BytesIO(raw)
|
|
1898
|
+
try:
|
|
1899
|
+
if zipfile.is_zipfile(bio):
|
|
1900
|
+
bio.seek(0)
|
|
1901
|
+
with zipfile.ZipFile(bio, "r") as zf:
|
|
1902
|
+
for info in zf.infolist():
|
|
1903
|
+
if info.is_dir():
|
|
1904
|
+
continue
|
|
1905
|
+
rel = _archive_member_relative_path(info.filename)
|
|
1906
|
+
if not rel:
|
|
1907
|
+
continue
|
|
1908
|
+
_write_bytes(rel, zf.read(info))
|
|
1909
|
+
return written
|
|
1910
|
+
except Exception:
|
|
1911
|
+
pass
|
|
1912
|
+
|
|
1913
|
+
with tarfile.open(fileobj=io.BytesIO(raw), mode="r:*") as tf:
|
|
1914
|
+
for member in tf.getmembers():
|
|
1915
|
+
if not member.isfile():
|
|
1916
|
+
continue
|
|
1917
|
+
rel = _archive_member_relative_path(member.name)
|
|
1918
|
+
if not rel:
|
|
1919
|
+
continue
|
|
1920
|
+
extracted = tf.extractfile(member)
|
|
1921
|
+
if extracted is None:
|
|
1922
|
+
continue
|
|
1923
|
+
_write_bytes(rel, extracted.read())
|
|
1924
|
+
return written
|
|
1925
|
+
|
|
1926
|
+
def _package_required_paths(entry: dict[str, object]) -> list[str]:
|
|
1927
|
+
rows: list[str] = []
|
|
1928
|
+
for item in entry.get("package_required_paths") or []:
|
|
1929
|
+
rel = _normalize_js_lib_asset_ref(str(item or ""))
|
|
1930
|
+
if rel:
|
|
1931
|
+
rows.append(rel)
|
|
1932
|
+
return rows
|
|
1933
|
+
|
|
1934
|
+
def _package_install_ready(install_root: Path, required_paths: list[str]) -> bool:
|
|
1935
|
+
if not install_root.exists():
|
|
1936
|
+
return False
|
|
1937
|
+
if required_paths:
|
|
1938
|
+
return all((install_root / rel).is_file() for rel in required_paths)
|
|
1939
|
+
try:
|
|
1940
|
+
return any(fp.is_file() for fp in install_root.rglob("*"))
|
|
1941
|
+
except Exception:
|
|
1942
|
+
return False
|
|
1943
|
+
|
|
1944
|
+
def _postprocess_offline_js_package(entry: dict[str, object], install_root: Path):
|
|
1945
|
+
action = str(entry.get("package_postprocess", "") or "").strip().lower()
|
|
1946
|
+
if action != "jszip-main-to-dist":
|
|
1947
|
+
return
|
|
1948
|
+
pkg_json = (install_root / "package.json").resolve()
|
|
1949
|
+
dist_fp = (install_root / "dist" / "jszip.min.js").resolve()
|
|
1950
|
+
if not pkg_json.exists():
|
|
1951
|
+
if not dist_fp.exists():
|
|
1952
|
+
return
|
|
1953
|
+
obj: dict[str, object] = {
|
|
1954
|
+
"name": "jszip",
|
|
1955
|
+
"version": "offline-local",
|
|
1956
|
+
"main": "./dist/jszip.min.js",
|
|
1957
|
+
"exports": {".": "./dist/jszip.min.js"},
|
|
1958
|
+
}
|
|
1959
|
+
else:
|
|
1960
|
+
try:
|
|
1961
|
+
raw = pkg_json.read_text(encoding="utf-8")
|
|
1962
|
+
loaded = json.loads(raw)
|
|
1963
|
+
if not isinstance(loaded, dict):
|
|
1964
|
+
return
|
|
1965
|
+
obj = loaded
|
|
1966
|
+
except Exception:
|
|
1967
|
+
return
|
|
1968
|
+
changed = False
|
|
1969
|
+
if obj.get("main") != "./dist/jszip.min.js":
|
|
1970
|
+
obj["main"] = "./dist/jszip.min.js"
|
|
1971
|
+
changed = True
|
|
1972
|
+
exports = obj.get("exports")
|
|
1973
|
+
desired_exports = {".": "./dist/jszip.min.js"}
|
|
1974
|
+
if exports != desired_exports:
|
|
1975
|
+
obj["exports"] = desired_exports
|
|
1976
|
+
changed = True
|
|
1977
|
+
if changed or (not pkg_json.exists()):
|
|
1978
|
+
pkg_json.parent.mkdir(parents=True, exist_ok=True)
|
|
1979
|
+
pkg_json.write_text(json_dumps(obj, indent=2), encoding="utf-8")
|
|
1980
|
+
|
|
1981
|
+
def _ensure_offline_js_package(root: Path, entry: dict[str, object], force: bool = False) -> tuple[bool, str, str, str]:
|
|
1982
|
+
package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
|
|
1983
|
+
if not package_urls:
|
|
1984
|
+
return True, "", "", ""
|
|
1985
|
+
install_dir = _normalize_js_lib_asset_ref(str(entry.get("package_install_dir", "") or ""))
|
|
1986
|
+
if not install_dir:
|
|
1987
|
+
install_dir = _normalize_js_lib_asset_ref(str(entry.get("id", "") or "package"))
|
|
1988
|
+
if not install_dir:
|
|
1989
|
+
return False, "missing", "package install dir is empty", ""
|
|
1990
|
+
install_root = (root / install_dir).resolve()
|
|
1991
|
+
if not install_root.is_relative_to(root):
|
|
1992
|
+
return False, "missing", f"package install dir escapes js_lib: {install_dir}", install_dir
|
|
1993
|
+
required_paths = _package_required_paths(entry)
|
|
1994
|
+
if not force and install_root.exists():
|
|
1995
|
+
_postprocess_offline_js_package(entry, install_root)
|
|
1996
|
+
if _package_install_ready(install_root, required_paths):
|
|
1997
|
+
return True, "existing-package", "", install_dir
|
|
1998
|
+
try:
|
|
1999
|
+
if any(fp.is_file() for fp in install_root.rglob("*")):
|
|
2000
|
+
return True, "existing-package", "", install_dir
|
|
2001
|
+
except Exception:
|
|
2002
|
+
pass
|
|
2003
|
+
error = ""
|
|
2004
|
+
source = "missing"
|
|
2005
|
+
for url in package_urls:
|
|
2006
|
+
try:
|
|
2007
|
+
data, _ = _download_http_bytes(url, timeout=90.0)
|
|
2008
|
+
if len(data) < 200:
|
|
2009
|
+
error = f"archive too small from {url}"
|
|
2010
|
+
continue
|
|
2011
|
+
_extract_archive_to_dir(data, install_root)
|
|
2012
|
+
_postprocess_offline_js_package(entry, install_root)
|
|
2013
|
+
if _package_install_ready(install_root, required_paths):
|
|
2014
|
+
return True, url, "", install_dir
|
|
2015
|
+
error = f"package install incomplete from {url}"
|
|
2016
|
+
source = url
|
|
2017
|
+
except Exception as exc:
|
|
2018
|
+
error = trim(str(exc), 220)
|
|
2019
|
+
source = url
|
|
2020
|
+
return False, source, error, install_dir
|
|
2021
|
+
|
|
1621
2022
|
def _render_offline_js_catalog_md() -> str:
|
|
1622
2023
|
rows = [
|
|
1623
2024
|
"# Offline JS Library Catalog",
|
|
1624
2025
|
"",
|
|
1625
2026
|
"Pre-fetched common JS libraries for offline HTML deliverables.",
|
|
2027
|
+
"Additional local `.js/.mjs/.cjs` files placed anywhere under `js_lib/` are auto-indexed in `index.json` and can be served by relative path.",
|
|
1626
2028
|
"",
|
|
1627
|
-
"| id |
|
|
1628
|
-
"
|
|
2029
|
+
"| id | relative path | file urls | package urls |",
|
|
2030
|
+
"|---|---|---|---|",
|
|
1629
2031
|
]
|
|
1630
2032
|
for row in OFFLINE_JS_LIB_CATALOG:
|
|
1631
2033
|
lib_id = str(row.get("id", "") or "").strip()
|
|
1632
|
-
filename = str(row.get("filename", "") or "")
|
|
2034
|
+
filename = _offline_js_entry_relative_path(row, str(row.get("filename", "") or lib_id or "lib.js"))
|
|
1633
2035
|
urls = [str(x).strip() for x in (row.get("urls") or []) if str(x).strip()]
|
|
1634
|
-
|
|
2036
|
+
package_urls = [str(x).strip() for x in (row.get("package_urls") or []) if str(x).strip()]
|
|
2037
|
+
rows.append(f"| `{lib_id}` | `{filename}` | {'<br>'.join(urls)} | {'<br>'.join(package_urls)} |")
|
|
1635
2038
|
return "\n".join(rows) + "\n"
|
|
1636
2039
|
|
|
1637
2040
|
def load_offline_js_lib_index(js_root: Path) -> dict:
|
|
@@ -1645,26 +2048,78 @@ def load_offline_js_lib_index(js_root: Path) -> dict:
|
|
|
1645
2048
|
except Exception:
|
|
1646
2049
|
return {}
|
|
1647
2050
|
|
|
1648
|
-
def ensure_offline_js_libs(
|
|
2051
|
+
def ensure_offline_js_libs(
|
|
2052
|
+
workdir: Path = WORKDIR,
|
|
2053
|
+
force: bool = False,
|
|
2054
|
+
verbose: bool = False,
|
|
2055
|
+
no_connection_deadline: float = 60.0,
|
|
2056
|
+
) -> dict:
|
|
1649
2057
|
root = offline_js_lib_root(workdir)
|
|
1650
2058
|
root.mkdir(parents=True, exist_ok=True)
|
|
1651
2059
|
fetched = 0
|
|
1652
2060
|
available = 0
|
|
1653
2061
|
missing = 0
|
|
2062
|
+
_dl_start = time.time()
|
|
2063
|
+
_deadline_hit = False
|
|
2064
|
+
total_catalog = len(OFFLINE_JS_LIB_CATALOG)
|
|
2065
|
+
if verbose:
|
|
2066
|
+
print(f"[js_lib] Starting JS library download ({total_catalog} files)...", flush=True)
|
|
1654
2067
|
rows: list[dict] = []
|
|
2068
|
+
known_relative_paths: set[str] = set()
|
|
1655
2069
|
for entry in OFFLINE_JS_LIB_CATALOG:
|
|
1656
2070
|
lib_id = str(entry.get("id", "") or "").strip() or "lib"
|
|
1657
2071
|
filename = _safe_js_filename(str(entry.get("filename", "") or f"{lib_id}.js"), f"{lib_id}.js")
|
|
2072
|
+
relative_path = _offline_js_entry_relative_path(entry, filename)
|
|
1658
2073
|
urls = [str(x).strip() for x in (entry.get("urls") or []) if str(x).strip()]
|
|
2074
|
+
package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
|
|
1659
2075
|
match_tokens = [str(x).strip().lower() for x in (entry.get("match_tokens") or []) if str(x).strip()]
|
|
1660
|
-
target = root /
|
|
2076
|
+
target = (root / relative_path).resolve()
|
|
2077
|
+
if not target.is_relative_to(root):
|
|
2078
|
+
rows.append(
|
|
2079
|
+
{
|
|
2080
|
+
"id": lib_id,
|
|
2081
|
+
"filename": filename,
|
|
2082
|
+
"available": False,
|
|
2083
|
+
"size": 0,
|
|
2084
|
+
"sha256": "",
|
|
2085
|
+
"source": "missing",
|
|
2086
|
+
"error": f"relative path escapes js_lib: {relative_path}",
|
|
2087
|
+
"match_tokens": match_tokens,
|
|
2088
|
+
"urls": urls,
|
|
2089
|
+
"package_urls": package_urls,
|
|
2090
|
+
"relative_path": relative_path,
|
|
2091
|
+
"package_install_dir": "",
|
|
2092
|
+
"package_required_paths": [],
|
|
2093
|
+
"package_available": not package_urls,
|
|
2094
|
+
"package_size": 0,
|
|
2095
|
+
"catalog": True,
|
|
2096
|
+
}
|
|
2097
|
+
)
|
|
2098
|
+
missing += 1
|
|
2099
|
+
continue
|
|
2100
|
+
known_relative_paths.add(relative_path)
|
|
1661
2101
|
source = "existing"
|
|
1662
2102
|
error = ""
|
|
1663
|
-
|
|
1664
|
-
|
|
2103
|
+
effective_target = target
|
|
2104
|
+
file_ok = bool(target.exists() and target.is_file() and target.stat().st_size > 40 and (not force))
|
|
2105
|
+
if (not file_ok) and (not force):
|
|
2106
|
+
resolved_existing = _resolve_js_lib_asset_path(root, relative_path)
|
|
2107
|
+
if resolved_existing and resolved_existing.exists() and resolved_existing.is_file() and resolved_existing.stat().st_size > 40:
|
|
2108
|
+
effective_target = resolved_existing
|
|
2109
|
+
file_ok = True
|
|
2110
|
+
source = "existing-local"
|
|
2111
|
+
if not file_ok and urls:
|
|
2112
|
+
if fetched == 0 and (time.time() - _dl_start) > no_connection_deadline:
|
|
2113
|
+
_deadline_hit = True
|
|
2114
|
+
if verbose:
|
|
2115
|
+
print(f"[js_lib] No connection after {no_connection_deadline:.0f}s — skipping remaining downloads.", flush=True)
|
|
2116
|
+
break
|
|
2117
|
+
if verbose:
|
|
2118
|
+
print(f"[js_lib] Downloading {filename}...", flush=True)
|
|
1665
2119
|
for url in urls:
|
|
1666
2120
|
try:
|
|
1667
|
-
|
|
2121
|
+
_timeout = 12.0 if fetched == 0 else 35.0
|
|
2122
|
+
data, _ = _download_http_bytes(url, timeout=_timeout)
|
|
1668
2123
|
if len(data) < 40:
|
|
1669
2124
|
error = f"download too small from {url}"
|
|
1670
2125
|
continue
|
|
@@ -1672,39 +2127,67 @@ def ensure_offline_js_libs(workdir: Path = WORKDIR, force: bool = False) -> dict
|
|
|
1672
2127
|
if "<html" in probe and "<script" not in probe and "tailwind" not in probe:
|
|
1673
2128
|
error = f"unexpected html payload from {url}"
|
|
1674
2129
|
continue
|
|
2130
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1675
2131
|
target.write_bytes(data)
|
|
1676
|
-
|
|
2132
|
+
effective_target = target
|
|
2133
|
+
file_ok = True
|
|
1677
2134
|
source = url
|
|
1678
2135
|
fetched += 1
|
|
1679
2136
|
break
|
|
1680
2137
|
except Exception as exc:
|
|
1681
2138
|
error = trim(str(exc), 220)
|
|
1682
|
-
if not
|
|
2139
|
+
if not file_ok:
|
|
1683
2140
|
source = "missing"
|
|
2141
|
+
package_ok, package_source, package_error, package_install_dir = _ensure_offline_js_package(root, entry, force=force)
|
|
2142
|
+
if package_urls and package_source and package_source not in {"existing-package", "missing"}:
|
|
2143
|
+
fetched += 1
|
|
2144
|
+
if package_error:
|
|
2145
|
+
error = package_error if not error else f"{error}; {package_error}"
|
|
2146
|
+
ok = bool((file_ok or (not urls)) and package_ok and effective_target.exists() and effective_target.is_file() and effective_target.stat().st_size > 40)
|
|
1684
2147
|
if ok:
|
|
1685
2148
|
available += 1
|
|
1686
2149
|
else:
|
|
1687
2150
|
missing += 1
|
|
2151
|
+
if package_urls and package_source == "missing" and not source.strip():
|
|
2152
|
+
source = "missing"
|
|
2153
|
+
package_root = (root / package_install_dir).resolve() if package_install_dir else None
|
|
1688
2154
|
rows.append(
|
|
1689
2155
|
{
|
|
1690
2156
|
"id": lib_id,
|
|
1691
2157
|
"filename": filename,
|
|
1692
2158
|
"available": ok,
|
|
1693
|
-
"size": int(
|
|
1694
|
-
"sha256": _sha256_file(
|
|
1695
|
-
"source": source,
|
|
2159
|
+
"size": int(effective_target.stat().st_size) if effective_target.exists() else 0,
|
|
2160
|
+
"sha256": _sha256_file(effective_target) if effective_target.exists() else "",
|
|
2161
|
+
"source": source if source != "existing" or file_ok else package_source or source,
|
|
1696
2162
|
"error": error,
|
|
1697
2163
|
"match_tokens": match_tokens,
|
|
1698
2164
|
"urls": urls,
|
|
2165
|
+
"package_urls": package_urls,
|
|
2166
|
+
"relative_path": relative_path,
|
|
2167
|
+
"resolved_path": effective_target.relative_to(root).as_posix() if effective_target.exists() else "",
|
|
2168
|
+
"package_install_dir": package_install_dir,
|
|
2169
|
+
"package_required_paths": _package_required_paths(entry),
|
|
2170
|
+
"package_available": package_ok,
|
|
2171
|
+
"package_size": _path_size_bytes(package_root) if package_root else 0,
|
|
2172
|
+
"package_source": package_source,
|
|
2173
|
+
"catalog": True,
|
|
1699
2174
|
}
|
|
1700
2175
|
)
|
|
2176
|
+
extra_rows = _discover_extra_js_lib_files(root, known_relative_paths)
|
|
2177
|
+
rows.extend(extra_rows)
|
|
2178
|
+
if verbose:
|
|
2179
|
+
_status = "skipped (no connection)" if _deadline_hit else f"{fetched} downloaded"
|
|
2180
|
+
print(f"[js_lib] Done: {available}/{len(rows)} available, {_status}.", flush=True)
|
|
1701
2181
|
payload = {
|
|
1702
2182
|
"generated_at": int(now_ts()),
|
|
1703
2183
|
"js_lib_root": str(root),
|
|
1704
|
-
"total": len(
|
|
2184
|
+
"total": len(rows),
|
|
1705
2185
|
"available": available,
|
|
1706
2186
|
"missing": missing,
|
|
1707
2187
|
"fetched": fetched,
|
|
2188
|
+
"catalog_total": len(OFFLINE_JS_LIB_CATALOG),
|
|
2189
|
+
"catalog_missing": missing,
|
|
2190
|
+
"extra_local": len(extra_rows),
|
|
1708
2191
|
"libs": rows,
|
|
1709
2192
|
}
|
|
1710
2193
|
(root / OFFLINE_JS_LIB_INDEX_FILE).write_text(json_dumps(payload, indent=2), encoding="utf-8")
|
|
@@ -3666,9 +4149,13 @@ class TodoManager:
|
|
|
3666
4149
|
row["owner"] = owner
|
|
3667
4150
|
if key:
|
|
3668
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
|
|
3669
4156
|
validated.append(row)
|
|
3670
|
-
if len(validated) >
|
|
3671
|
-
raise ValueError("max
|
|
4157
|
+
if len(validated) > 40:
|
|
4158
|
+
raise ValueError("max 40 todos")
|
|
3672
4159
|
if validated and not any(x["status"] == "in_progress" for x in validated):
|
|
3673
4160
|
for row in validated:
|
|
3674
4161
|
if row["status"] == "pending":
|
|
@@ -4311,6 +4798,7 @@ EMBEDDED_SKILLS_ARCHIVE_FILES = [
|
|
|
4311
4798
|
"skills/generated/upload-office-parser/SKILL.md",
|
|
4312
4799
|
"skills/generated/upload-parsers-capabilities.json",
|
|
4313
4800
|
"skills/generated/upload-tabular-parser/SKILL.md",
|
|
4801
|
+
"skills/generated/upload-image-parser/SKILL.md",
|
|
4314
4802
|
"skills/mcp-builder/SKILL.md",
|
|
4315
4803
|
"skills/pdf/SKILL.md",
|
|
4316
4804
|
"skills/skills_Gen/SKILL.md",
|
|
@@ -4469,6 +4957,36 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
|
|
|
4469
4957
|
## Notes
|
|
4470
4958
|
- The backend automatically parses `.doc`, `.docx`, `.ppt`, `.pptx`.
|
|
4471
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.
|
|
4472
4990
|
"""
|
|
4473
4991
|
cap_json = json_dumps(
|
|
4474
4992
|
{
|
|
@@ -4479,6 +4997,7 @@ Use this skill when the user uploads Word/PowerPoint documents and needs content
|
|
|
4479
4997
|
)
|
|
4480
4998
|
_write_text_if_changed(generated_root / "upload-tabular-parser" / "SKILL.md", tabular_skill)
|
|
4481
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)
|
|
4482
5001
|
_write_text_if_changed(generated_root / "upload-parsers-capabilities.json", cap_json)
|
|
4483
5002
|
|
|
4484
5003
|
def ensure_generated_image_coding_feedback_skill(skills_root: Path):
|
|
@@ -4501,12 +5020,12 @@ Use this skill when the task depends on image understanding in a coding workflow
|
|
|
4501
5020
|
- Generated image(s): current output from app, script, or model generation pipeline.
|
|
4502
5021
|
- Code scope: file paths, rendering command, and runtime constraints.
|
|
4503
5022
|
|
|
4504
|
-
##
|
|
4505
|
-
1.
|
|
4506
|
-
2.
|
|
4507
|
-
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):
|
|
4508
5027
|
- deterministic metadata checks (size/aspect/background),
|
|
4509
|
-
- text checks
|
|
5028
|
+
- text checks via OCR tools (e.g., `tesseract`),
|
|
4510
5029
|
- simple pixel-region checks from locally rendered output.
|
|
4511
5030
|
4. Always report confidence level (`high|medium|low`) based on signal quality.
|
|
4512
5031
|
|
|
@@ -5904,6 +6423,7 @@ def ensure_generated_runtime_skills_manifest(skills_root: Path):
|
|
|
5904
6423
|
"skills_Gen/knowledge_snapshot.json",
|
|
5905
6424
|
"generated/upload-tabular-parser/SKILL.md",
|
|
5906
6425
|
"generated/upload-office-parser/SKILL.md",
|
|
6426
|
+
"generated/upload-image-parser/SKILL.md",
|
|
5907
6427
|
"generated/image-coding-feedback-loop/SKILL.md",
|
|
5908
6428
|
"generated/execution-degradation-recovery/SKILL.md",
|
|
5909
6429
|
"generated/deep-research-orchestrator/SKILL.md",
|
|
@@ -9043,7 +9563,12 @@ TOOLS = [
|
|
|
9043
9563
|
),
|
|
9044
9564
|
tool_def(
|
|
9045
9565
|
"query_knowledge_library",
|
|
9046
|
-
|
|
9566
|
+
(
|
|
9567
|
+
"Query the RAG knowledge library for grounded document references and background knowledge. "
|
|
9568
|
+
"Call this BEFORE answering questions that require domain expertise, factual grounding, "
|
|
9569
|
+
"or synthesis from imported documents. "
|
|
9570
|
+
"Pass an empty query to check library status only."
|
|
9571
|
+
),
|
|
9047
9572
|
{
|
|
9048
9573
|
"query": {"type": "string"},
|
|
9049
9574
|
"top_k": {"type": "integer"},
|
|
@@ -9374,6 +9899,8 @@ class SessionState:
|
|
|
9374
9899
|
self.runtime_task_judgement = ""
|
|
9375
9900
|
self.runtime_task_type = ""
|
|
9376
9901
|
self.runtime_task_complexity = ""
|
|
9902
|
+
self.runtime_complexity_floor = ""
|
|
9903
|
+
self.runtime_task_level_floor = 0
|
|
9377
9904
|
self.runtime_scale_preference = "balanced"
|
|
9378
9905
|
self.runtime_direct_objective = ""
|
|
9379
9906
|
self.runtime_reclassify_goal = ""
|
|
@@ -10454,6 +10981,12 @@ class SessionState:
|
|
|
10454
10981
|
self.runtime_reclassify_required = bool(
|
|
10455
10982
|
raw.get("runtime_reclassify_required", self.runtime_reclassify_required)
|
|
10456
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
|
+
)
|
|
10457
10990
|
self.runtime_goal_reset_pending = bool(
|
|
10458
10991
|
raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
|
|
10459
10992
|
)
|
|
@@ -10616,6 +11149,8 @@ class SessionState:
|
|
|
10616
11149
|
"runtime_direct_objective": trim(str(self.runtime_direct_objective or ""), 800),
|
|
10617
11150
|
"runtime_reclassify_goal": trim(str(self.runtime_reclassify_goal or ""), 4000),
|
|
10618
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),
|
|
10619
11154
|
"runtime_goal_reset_pending": bool(self.runtime_goal_reset_pending),
|
|
10620
11155
|
"runtime_plan_mode_needed": bool(self.runtime_plan_mode_needed),
|
|
10621
11156
|
"runtime_plan_approved": bool(self.runtime_plan_approved),
|
|
@@ -10780,6 +11315,39 @@ class SessionState:
|
|
|
10780
11315
|
self.runtime_code_reference_meta = {}
|
|
10781
11316
|
return removed_hints
|
|
10782
11317
|
|
|
11318
|
+
def _reset_blackboard_plan_state_locked(self) -> None:
|
|
11319
|
+
"""Clear plan/todo/skills state from a completed run so the next run starts fresh.
|
|
11320
|
+
|
|
11321
|
+
Called from submit_user_message when a new user request arrives after a
|
|
11322
|
+
previous run finished (running=False, not awaiting plan choice).
|
|
11323
|
+
Prevents the manager from seeing status=COMPLETED + all todos done and
|
|
11324
|
+
immediately routing to 'finish' again on the very first round.
|
|
11325
|
+
"""
|
|
11326
|
+
bb = self._ensure_blackboard()
|
|
11327
|
+
bb["project_todos"] = []
|
|
11328
|
+
bb["plan_steps"] = []
|
|
11329
|
+
bb["plan_step_cursor"] = 0
|
|
11330
|
+
bb["plan_step_total"] = 0
|
|
11331
|
+
bb["status"] = ""
|
|
11332
|
+
bb["approval"] = ""
|
|
11333
|
+
bb["plan_findings"] = ""
|
|
11334
|
+
bb["plan_proposal"] = ""
|
|
11335
|
+
bb["plan_risks"] = ""
|
|
11336
|
+
bb["loaded_skills"] = {}
|
|
11337
|
+
bb["loaded_skills_goal_sig"] = ""
|
|
11338
|
+
self.blackboard = bb
|
|
11339
|
+
try:
|
|
11340
|
+
self.todo.items = []
|
|
11341
|
+
except Exception:
|
|
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
|
|
11350
|
+
|
|
10783
11351
|
def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
|
|
10784
11352
|
payload = dict(data or {})
|
|
10785
11353
|
if self._sanitize_agent_bubble_role(payload.get("agent_role", "")):
|
|
@@ -11133,7 +11701,10 @@ class SessionState:
|
|
|
11133
11701
|
skill_desc = str(skill_row.get("description", "-")).strip()
|
|
11134
11702
|
inject_msg = (
|
|
11135
11703
|
f"<loaded-skill name=\"{skill_key}\">\n"
|
|
11136
|
-
f"A skill has been loaded.
|
|
11704
|
+
f"A skill has been loaded. IMPORTANT: This skill's workflow, tools, and commands "
|
|
11705
|
+
f"OVERRIDE the plan's implementation approach for any step where it applies. "
|
|
11706
|
+
f"Read the full instructions below and follow them exactly — do NOT substitute a "
|
|
11707
|
+
f"different tool, library, or language unless the skill explicitly allows it.\n"
|
|
11137
11708
|
f"{trim(skill_text, 12000)}\n"
|
|
11138
11709
|
f"</loaded-skill>"
|
|
11139
11710
|
)
|
|
@@ -11594,14 +12165,28 @@ class SessionState:
|
|
|
11594
12165
|
)
|
|
11595
12166
|
return (
|
|
11596
12167
|
f"ACTIVE SKILLS: {names}. "
|
|
11597
|
-
"Follow loaded skill instructions
|
|
11598
|
-
f"
|
|
12168
|
+
"Follow the loaded skill instructions for the current step. "
|
|
12169
|
+
f"When moving to a different step that needs a DIFFERENT skill, call load_skill to switch "
|
|
12170
|
+
f"(or unload the current one first if it's no longer needed). "
|
|
12171
|
+
f"{skill_count} skills available total. "
|
|
11599
12172
|
)
|
|
11600
12173
|
return (
|
|
11601
12174
|
f"SKILL SYSTEM: {skill_count} skills available. "
|
|
11602
|
-
"
|
|
11603
|
-
"
|
|
11604
|
-
|
|
12175
|
+
"Skills are loaded ON-DEMAND — decide when you need one based on the CURRENT step, not upfront. "
|
|
12176
|
+
"For specialized output (reports, slides/PPT, deep research, code review, PDF analysis): "
|
|
12177
|
+
"call list_skills to discover options, then load_skill to activate the right one. "
|
|
12178
|
+
"Load a skill AT THE MOMENT you begin the step that requires it. "
|
|
12179
|
+
"Unload it (via unload_skill) when moving to a different step that needs a different skill. "
|
|
12180
|
+
"For simple tasks, direct questions, and multimodal analysis, do NOT load skills. "
|
|
12181
|
+
)
|
|
12182
|
+
|
|
12183
|
+
def _skills_awareness_block(self, for_role: str = "developer") -> str:
|
|
12184
|
+
"""Canonical skills-awareness block shared by single, sync, and plan-mode.
|
|
12185
|
+
Returns: loaded-skills hint + newline + 'Skills:\\n<catalog>'
|
|
12186
|
+
Keeps all three modes in sync — change here propagates everywhere.
|
|
12187
|
+
"""
|
|
12188
|
+
hint = self._loaded_skills_prompt_hint(for_role=for_role)
|
|
12189
|
+
return f"{hint}\nSkills:\n{self.skills.descriptions()}\n"
|
|
11605
12190
|
|
|
11606
12191
|
def _refresh_runtime_code_reference(self, text: str):
|
|
11607
12192
|
cb = getattr(self, "reference_prepare_callback", None)
|
|
@@ -11663,10 +12248,40 @@ class SessionState:
|
|
|
11663
12248
|
header += " [" + ", ".join(tags) + "]"
|
|
11664
12249
|
return (
|
|
11665
12250
|
f"{header}:\n"
|
|
11666
|
-
"
|
|
11667
|
-
"
|
|
11668
|
-
"
|
|
11669
|
-
"
|
|
12251
|
+
"Global TF-Graph_IDF RAG library for imported documents, PDFs, and research material. "
|
|
12252
|
+
"IMPORTANT: When the task involves a topic you may have documents for — research, analysis, "
|
|
12253
|
+
"fact-checking, synthesis — FIRST call query_knowledge_library(query='<topic>', top_k=8) "
|
|
12254
|
+
"to retrieve grounded references BEFORE generating your answer. "
|
|
12255
|
+
"Use route='hybrid' for best recall on broad topics; route='fast' for keyword lookups. "
|
|
12256
|
+
"Do not infer readiness from session/files or uploads — query the library directly."
|
|
12257
|
+
)
|
|
12258
|
+
|
|
12259
|
+
def _multimodal_capability_block(self) -> str:
|
|
12260
|
+
"""Return a brief system-prompt note about native multimodal capabilities.
|
|
12261
|
+
|
|
12262
|
+
Injected into system prompt so the model knows it can directly analyze
|
|
12263
|
+
images/audio/video rather than falling back to text-only workarounds.
|
|
12264
|
+
Returns empty string when the active model has no multimodal input caps.
|
|
12265
|
+
"""
|
|
12266
|
+
try:
|
|
12267
|
+
caps = self._capabilities_from_profile()
|
|
12268
|
+
except Exception:
|
|
12269
|
+
return ""
|
|
12270
|
+
types = []
|
|
12271
|
+
if caps.get("input_image"):
|
|
12272
|
+
types.append("images")
|
|
12273
|
+
if caps.get("input_audio"):
|
|
12274
|
+
types.append("audio")
|
|
12275
|
+
if caps.get("input_video"):
|
|
12276
|
+
types.append("video")
|
|
12277
|
+
if not types:
|
|
12278
|
+
return ""
|
|
12279
|
+
joined = "/".join(types)
|
|
12280
|
+
return (
|
|
12281
|
+
f"MULTIMODAL: This model supports native {joined} analysis. "
|
|
12282
|
+
f"When {joined} are attached or loaded via read_file, analyze them DIRECTLY "
|
|
12283
|
+
"using your built-in perception capabilities — do not describe them as "
|
|
12284
|
+
"inaccessible or attempt text-only workarounds. "
|
|
11670
12285
|
)
|
|
11671
12286
|
|
|
11672
12287
|
def _code_library_prompt_block(self) -> str:
|
|
@@ -11747,8 +12362,14 @@ class SessionState:
|
|
|
11747
12362
|
plan_ctx = self._plan_steps_context_for_manager()
|
|
11748
12363
|
if plan_ctx:
|
|
11749
12364
|
plan_steps_block = f"{plan_ctx}\n"
|
|
12365
|
+
mm_block = self._multimodal_capability_block()
|
|
12366
|
+
mm_hint = f"{mm_block}\n" if mm_block else ""
|
|
11750
12367
|
return (
|
|
11751
|
-
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
12368
|
+
f"You are a coding agent. Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
|
|
12369
|
+
f"Offline JS libraries root: $JS_LIB_ROOT. "
|
|
12370
|
+
f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
|
|
12371
|
+
f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS require) or pptxgen.bundle.js (browser). "
|
|
12372
|
+
f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
|
|
11752
12373
|
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
11753
12374
|
f"budget={'unlimited' if budget <= 0 else budget}. "
|
|
11754
12375
|
f"Context limit ~{self.context_token_upper_bound} tokens. "
|
|
@@ -11756,6 +12377,7 @@ class SessionState:
|
|
|
11756
12377
|
"Use tools to inspect, edit, and execute. "
|
|
11757
12378
|
"Call finish_current_task when done. "
|
|
11758
12379
|
f"{skill_hint}"
|
|
12380
|
+
f"{mm_hint}"
|
|
11759
12381
|
f"{plan_steps_block}"
|
|
11760
12382
|
f"{html_block}"
|
|
11761
12383
|
f"{research_block}"
|
|
@@ -11911,7 +12533,7 @@ class SessionState:
|
|
|
11911
12533
|
o.pop("cons", None)
|
|
11912
12534
|
o.pop("risk", None)
|
|
11913
12535
|
elif tier >= 3:
|
|
11914
|
-
# Minimal: only phase, chosen, steps
|
|
12536
|
+
# Minimal: only phase, chosen, steps — but preserve project_todos
|
|
11915
12537
|
phase = plan.get("phase", "")
|
|
11916
12538
|
chosen = plan.get("chosen", "")
|
|
11917
12539
|
steps = plan.get("steps", [])
|
|
@@ -11927,7 +12549,10 @@ class SessionState:
|
|
|
11927
12549
|
o for o in options if isinstance(o, dict) and o.get("id") == chosen_id
|
|
11928
12550
|
]
|
|
11929
12551
|
if tier >= 3:
|
|
11930
|
-
|
|
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 = {}
|
|
11931
12556
|
|
|
11932
12557
|
def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
|
|
11933
12558
|
metrics = self._context_budget_metrics()
|
|
@@ -12329,16 +12954,13 @@ class SessionState:
|
|
|
12329
12954
|
meta = {"filtered": False, "reason": "", "original_chars": len(raw)}
|
|
12330
12955
|
if not clean:
|
|
12331
12956
|
return raw, meta
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
and len(clean) >= int(RAW_TOOLCALL_TEXT_FILTER_THRESHOLD)
|
|
12335
|
-
and self._looks_like_raw_toolcall_blob(clean)
|
|
12336
|
-
):
|
|
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):
|
|
12337
12959
|
note = (
|
|
12338
|
-
"[toolcall payload omitted: detected
|
|
12339
|
-
"
|
|
12960
|
+
"[toolcall payload omitted: detected inline <toolcall> text. "
|
|
12961
|
+
"Please regenerate a compact structured tool call.]"
|
|
12340
12962
|
)
|
|
12341
|
-
meta.update({"filtered": True, "reason": "
|
|
12963
|
+
meta.update({"filtered": True, "reason": "raw_toolcall"})
|
|
12342
12964
|
return note, meta
|
|
12343
12965
|
if len(raw) > int(ASSISTANT_TEXT_PERSIST_MAX_CHARS):
|
|
12344
12966
|
clipped = trim(raw, int(ASSISTANT_TEXT_PERSIST_MAX_CHARS))
|
|
@@ -13540,6 +14162,11 @@ class SessionState:
|
|
|
13540
14162
|
if low.startswith("/workspace/"):
|
|
13541
14163
|
rel = raw[len("/workspace/") :].lstrip("/")
|
|
13542
14164
|
return rel or "."
|
|
14165
|
+
if low in {"/js_lib", "/js_lib/"}:
|
|
14166
|
+
return ".__js_lib__"
|
|
14167
|
+
if low.startswith("/js_lib/"):
|
|
14168
|
+
rel = raw[len("/js_lib/") :].lstrip("/")
|
|
14169
|
+
return f".__js_lib__/{rel}" if rel else ".__js_lib__"
|
|
13543
14170
|
if low in {SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}:
|
|
13544
14171
|
return ".__skills__"
|
|
13545
14172
|
if low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/"):
|
|
@@ -13563,9 +14190,10 @@ class SessionState:
|
|
|
13563
14190
|
return ""
|
|
13564
14191
|
low = txt.lower()
|
|
13565
14192
|
if (
|
|
13566
|
-
low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}
|
|
14193
|
+
low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/", "/js_lib", "/js_lib/"}
|
|
13567
14194
|
or low.startswith("/workspace/")
|
|
13568
14195
|
or low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/")
|
|
14196
|
+
or low.startswith("/js_lib/")
|
|
13569
14197
|
):
|
|
13570
14198
|
return ""
|
|
13571
14199
|
return (
|
|
@@ -13578,6 +14206,9 @@ class SessionState:
|
|
|
13578
14206
|
if normalized == ".__skills__" or normalized.startswith(".__skills__/"):
|
|
13579
14207
|
rel = normalized[len(".__skills__") :].lstrip("/")
|
|
13580
14208
|
return safe_path(rel or ".", self.skills.skills_root)
|
|
14209
|
+
if normalized == ".__js_lib__" or normalized.startswith(".__js_lib__/"):
|
|
14210
|
+
rel = normalized[len(".__js_lib__") :].lstrip("/")
|
|
14211
|
+
return safe_path(rel or ".", self.js_lib_root)
|
|
13581
14212
|
return safe_path(normalized, self.files_root)
|
|
13582
14213
|
|
|
13583
14214
|
def _session_rel(self, path: Path) -> str:
|
|
@@ -13585,6 +14216,13 @@ class SessionState:
|
|
|
13585
14216
|
root = self.files_root.resolve()
|
|
13586
14217
|
if target.is_relative_to(root):
|
|
13587
14218
|
return target.relative_to(root).as_posix()
|
|
14219
|
+
try:
|
|
14220
|
+
js_lib_root = self.js_lib_root.resolve()
|
|
14221
|
+
if target.is_relative_to(js_lib_root):
|
|
14222
|
+
rel = target.relative_to(js_lib_root).as_posix()
|
|
14223
|
+
return f"/js_lib/{rel}".replace("//", "/")
|
|
14224
|
+
except Exception:
|
|
14225
|
+
pass
|
|
13588
14226
|
try:
|
|
13589
14227
|
skills_root = self.skills.skills_root.resolve()
|
|
13590
14228
|
if target.is_relative_to(skills_root):
|
|
@@ -13962,8 +14600,20 @@ class SessionState:
|
|
|
13962
14600
|
}
|
|
13963
14601
|
|
|
13964
14602
|
def _safe_upload_name(self, filename: str) -> str:
|
|
14603
|
+
# Use Path().name to strip any directory component first.
|
|
13965
14604
|
raw = Path(str(filename or "upload.bin")).name
|
|
13966
|
-
|
|
14605
|
+
# Only remove characters that are genuinely illegal or dangerous on
|
|
14606
|
+
# filesystems (path separators, null byte, control chars, Windows
|
|
14607
|
+
# reserved chars). Unicode letters/CJK/etc. are left untouched so
|
|
14608
|
+
# filenames like "我的数据.xlsx" remain readable.
|
|
14609
|
+
safe = re.sub(r'[/\\\x00-\x1f\x7f:*?"<>|]', "_", raw)
|
|
14610
|
+
safe = safe.strip(". ") # avoid hidden files (leading dot) and trailing issues
|
|
14611
|
+
# Enforce a byte-length ceiling safe across all filesystems (255 bytes max).
|
|
14612
|
+
# Trim the stem if needed while preserving the extension.
|
|
14613
|
+
if len(safe.encode("utf-8", errors="replace")) > 240:
|
|
14614
|
+
ext = Path(safe).suffix # e.g. ".xlsx"
|
|
14615
|
+
stem = safe[: max(1, 200 - len(ext))]
|
|
14616
|
+
safe = stem + ext
|
|
13967
14617
|
return safe or f"upload_{int(now_ts())}.bin"
|
|
13968
14618
|
|
|
13969
14619
|
def _decode_text_bytes(self, data: bytes) -> str:
|
|
@@ -15964,15 +16614,22 @@ class SessionState:
|
|
|
15964
16614
|
skills_root = str(self.skills.skills_root.resolve())
|
|
15965
16615
|
except Exception:
|
|
15966
16616
|
skills_root = str(self.skills.skills_root)
|
|
16617
|
+
try:
|
|
16618
|
+
js_lib_root = str(self.js_lib_root.resolve())
|
|
16619
|
+
except Exception:
|
|
16620
|
+
js_lib_root = str(self.js_lib_root)
|
|
15967
16621
|
mappings = [
|
|
15968
16622
|
("/workspace", workspace_root),
|
|
15969
16623
|
(SKILLS_VIRTUAL_PREFIX, skills_root),
|
|
16624
|
+
("/js_lib", js_lib_root),
|
|
15970
16625
|
]
|
|
15971
16626
|
# Auto-quote raw absolute paths that contain spaces (prevents word-splitting)
|
|
15972
16627
|
if " " in workspace_root and workspace_root not in ("/workspace",):
|
|
15973
16628
|
mappings.append((workspace_root, workspace_root))
|
|
15974
16629
|
if " " in skills_root and skills_root != SKILLS_VIRTUAL_PREFIX:
|
|
15975
16630
|
mappings.append((skills_root, skills_root))
|
|
16631
|
+
if " " in js_lib_root and js_lib_root != "/js_lib":
|
|
16632
|
+
mappings.append((js_lib_root, js_lib_root))
|
|
15976
16633
|
out: list[str] = []
|
|
15977
16634
|
mode = "plain"
|
|
15978
16635
|
i = 0
|
|
@@ -16313,6 +16970,7 @@ class SessionState:
|
|
|
16313
16970
|
proc_env["SESSION_FILES_ROOT"] = str(self.files_root)
|
|
16314
16971
|
proc_env["SKILLS_ROOT"] = str(self.skills.skills_root)
|
|
16315
16972
|
proc_env["CLOUDS_CODER_ROOT"] = str(self.root)
|
|
16973
|
+
proc_env["JS_LIB_ROOT"] = str(self.js_lib_root)
|
|
16316
16974
|
popen_kwargs = {
|
|
16317
16975
|
"shell": True,
|
|
16318
16976
|
"cwd": cwd,
|
|
@@ -16430,10 +17088,31 @@ class SessionState:
|
|
|
16430
17088
|
def _run_bash(self, command: str) -> str:
|
|
16431
17089
|
return self._run_shell_meta(command, self.files_root, 120)["output"]
|
|
16432
17090
|
|
|
17091
|
+
def _fuzzy_resolve_path(self, fp: Path) -> Path:
|
|
17092
|
+
"""If fp doesn't exist, try stripping spaces from the filename to find a close match.
|
|
17093
|
+
Handles the common model error of hallucinating spaces in Chinese/mixed filenames.
|
|
17094
|
+
Returns the resolved Path if found, otherwise the original fp unchanged."""
|
|
17095
|
+
if fp.exists():
|
|
17096
|
+
return fp
|
|
17097
|
+
stripped = fp.name.replace(" ", "")
|
|
17098
|
+
if stripped != fp.name:
|
|
17099
|
+
candidate = fp.parent / stripped
|
|
17100
|
+
if candidate.exists():
|
|
17101
|
+
return candidate
|
|
17102
|
+
try:
|
|
17103
|
+
query = fp.name.replace(" ", "").lower()
|
|
17104
|
+
for sibling in fp.parent.iterdir():
|
|
17105
|
+
if sibling.name.replace(" ", "").lower() == query:
|
|
17106
|
+
return sibling
|
|
17107
|
+
except Exception:
|
|
17108
|
+
pass
|
|
17109
|
+
return fp
|
|
17110
|
+
|
|
16433
17111
|
def _run_read(self, path: str, limit: int | None = None, offset: int | None = None) -> str:
|
|
16434
17112
|
try:
|
|
16435
17113
|
rel = self._normalize_tool_path_text(path)
|
|
16436
|
-
fp = self._session_path(rel)
|
|
17114
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17115
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16437
17116
|
# Multimodal: detect image/audio/video files and handle natively
|
|
16438
17117
|
ext = fp.suffix.lower() if fp.suffix else ""
|
|
16439
17118
|
if ext in IMAGE_EXTS:
|
|
@@ -16680,7 +17359,8 @@ class SessionState:
|
|
|
16680
17359
|
def _run_write(self, path: str, content: str) -> str:
|
|
16681
17360
|
try:
|
|
16682
17361
|
rel = self._normalize_tool_path_text(path)
|
|
16683
|
-
fp = self._session_path(rel)
|
|
17362
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17363
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16684
17364
|
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
16685
17365
|
fp.write_text(content, encoding="utf-8")
|
|
16686
17366
|
return f"Wrote {len(content)} bytes to {rel}"
|
|
@@ -16690,7 +17370,8 @@ class SessionState:
|
|
|
16690
17370
|
def _run_edit(self, path: str, old_text: str, new_text: str) -> str:
|
|
16691
17371
|
try:
|
|
16692
17372
|
rel = self._normalize_tool_path_text(path)
|
|
16693
|
-
fp = self._session_path(rel)
|
|
17373
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17374
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16694
17375
|
content = fp.read_text(encoding="utf-8")
|
|
16695
17376
|
if old_text not in content:
|
|
16696
17377
|
diag = self._edit_mismatch_diagnostic(content, old_text)
|
|
@@ -18445,7 +19126,7 @@ class SessionState:
|
|
|
18445
19126
|
board["project_todos"] = []
|
|
18446
19127
|
else:
|
|
18447
19128
|
clean_todos = []
|
|
18448
|
-
for pt in bb_src_todos[:
|
|
19129
|
+
for pt in bb_src_todos[:40]:
|
|
18449
19130
|
if not isinstance(pt, dict):
|
|
18450
19131
|
continue
|
|
18451
19132
|
clean_todos.append({
|
|
@@ -18514,6 +19195,15 @@ class SessionState:
|
|
|
18514
19195
|
goal_preview = trim(str(src.get("loaded_skills_goal_preview", "") or ""), 240)
|
|
18515
19196
|
if goal_preview:
|
|
18516
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
|
|
18517
19207
|
return board
|
|
18518
19208
|
|
|
18519
19209
|
def _normalize_failure_ledger(self, raw: dict) -> dict:
|
|
@@ -18941,9 +19631,17 @@ class SessionState:
|
|
|
18941
19631
|
self.blackboard["loaded_skills"] = preserved_skills
|
|
18942
19632
|
self.blackboard["loaded_skills_goal_sig"] = preserved_skills_sig
|
|
18943
19633
|
self.blackboard["loaded_skills_goal_preview"] = trim(str(goal or ""), 240)
|
|
18944
|
-
# Restore plan state if plan is
|
|
18945
|
-
|
|
18946
|
-
|
|
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
|
|
18947
19645
|
if isinstance(preserved_todos, list) and preserved_todos:
|
|
18948
19646
|
self.blackboard["project_todos"] = preserved_todos
|
|
18949
19647
|
if preserved_cursor is not None:
|
|
@@ -19440,35 +20138,36 @@ class SessionState:
|
|
|
19440
20138
|
if not current:
|
|
19441
20139
|
return False
|
|
19442
20140
|
text = (str(instruction or "") + " " + str(reason or "")).lower()
|
|
19443
|
-
# Patterns that indicate step completion
|
|
20141
|
+
# Patterns that indicate step completion — only BACKWARD-looking signals
|
|
20142
|
+
# (agent/manager explicitly says a step is done, NOT forward-looking dispatch instructions)
|
|
19444
20143
|
step_done_patterns = (
|
|
19445
20144
|
"审查通过", "通过审查", "已通过", "已完成", "完成了",
|
|
19446
|
-
"进入 step", "进入step", "enter step", "move to step",
|
|
19447
20145
|
"step completed", "step done", "step passed",
|
|
19448
|
-
"现在进入", "开始 step", "开始step",
|
|
19449
20146
|
"阶段完成", "阶段通过", "phase complete",
|
|
19450
|
-
|
|
19451
|
-
"
|
|
19452
|
-
"进入下一步", "next step", "proceed to step",
|
|
19453
|
-
"步骤 1.1 已完成", "步骤 1.2 已完成",
|
|
20147
|
+
"步骤 1.1 已完成", "步骤 1.2 已完成", "步骤 1.3 已完成",
|
|
20148
|
+
"步骤 1.4 已完成", "步骤 1.5 已完成",
|
|
19454
20149
|
)
|
|
19455
|
-
#
|
|
20150
|
+
# NOTE: intentionally excluded forward-looking dispatch patterns:
|
|
20151
|
+
# "现在执行步骤", "执行步骤 1.2", "执行步骤 1.3", "进入下一步", "next step",
|
|
20152
|
+
# "proceed to step", "进入 step", "开始 step" — these are manager dispatch
|
|
20153
|
+
# instructions, NOT evidence that the current step was completed.
|
|
19456
20154
|
import re
|
|
19457
20155
|
current_idx = int(current.get("plan_step_index", 0) or 0)
|
|
19458
|
-
#
|
|
20156
|
+
# Only advance when an agent explicitly says "进入 Step N" where N > current+1
|
|
20157
|
+
# (skipping ahead), NOT when manager dispatches the very next step.
|
|
19459
20158
|
next_step_pattern = re.search(
|
|
19460
20159
|
r'(?:进入|enter|move\s+to|start|proceed\s+to)\s*(?:step\s*)?(\d+)',
|
|
19461
20160
|
text, re.IGNORECASE
|
|
19462
20161
|
)
|
|
19463
20162
|
if next_step_pattern:
|
|
19464
20163
|
mentioned_step = int(next_step_pattern.group(1))
|
|
19465
|
-
if mentioned_step > current_idx +
|
|
20164
|
+
if mentioned_step > current_idx + 2: # must skip at least 2 ahead to be meaningful
|
|
19466
20165
|
return True
|
|
19467
|
-
#
|
|
19468
|
-
step_ref = re.search(r'
|
|
20166
|
+
# "完成步骤 1.1,开始 1.2" — only if explicitly marking current step done
|
|
20167
|
+
step_ref = re.search(r'(?:完成|finished|done)\s*步骤\s*1\.(\d+)', text)
|
|
19469
20168
|
if step_ref:
|
|
19470
20169
|
ref_sub = int(step_ref.group(1))
|
|
19471
|
-
if ref_sub
|
|
20170
|
+
if ref_sub == current_idx + 1: # explicitly says current step (1-based) is done
|
|
19472
20171
|
return True
|
|
19473
20172
|
return any(pat in text for pat in step_done_patterns)
|
|
19474
20173
|
|
|
@@ -19505,14 +20204,19 @@ class SessionState:
|
|
|
19505
20204
|
})
|
|
19506
20205
|
self.blackboard = bb
|
|
19507
20206
|
self._blackboard_touch()
|
|
19508
|
-
#
|
|
19509
|
-
|
|
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 "")
|
|
19510
20215
|
try:
|
|
19511
20216
|
_snap = self.todo.snapshot()
|
|
19512
|
-
_worker_owners = {"developer", "explorer", "reviewer"}
|
|
19513
20217
|
_clean = [
|
|
19514
20218
|
r for r in _snap
|
|
19515
|
-
if str(r.get("
|
|
20219
|
+
if str(r.get("parent_step_id", "") or "") != completed_step_id
|
|
19516
20220
|
or str(r.get("status", "") or "").lower() == "completed"
|
|
19517
20221
|
]
|
|
19518
20222
|
if len(_clean) < len(_snap):
|
|
@@ -19524,11 +20228,87 @@ class SessionState:
|
|
|
19524
20228
|
self._sync_todos_from_blackboard(reason=f"plan-step-advanced:{cursor + 1}", board=bb)
|
|
19525
20229
|
if next_step:
|
|
19526
20230
|
try:
|
|
19527
|
-
|
|
20231
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
19528
20232
|
except Exception:
|
|
19529
20233
|
pass
|
|
19530
20234
|
return True
|
|
19531
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
|
+
|
|
19532
20312
|
def _single_agent_plan_step_check(self, tool_results: list[dict]):
|
|
19533
20313
|
"""In single-agent mode, check if current plan step should be advanced based on tool results."""
|
|
19534
20314
|
bb = self._ensure_blackboard()
|
|
@@ -19602,6 +20382,7 @@ class SessionState:
|
|
|
19602
20382
|
_step_label = f"Step {_step_idx}" + (f"/{_total}" if _total else "")
|
|
19603
20383
|
_hint = (
|
|
19604
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"
|
|
19605
20386
|
"Call TodoWrite to set your task breakdown for this step "
|
|
19606
20387
|
"(3-5 subtask items, one marked in_progress) before proceeding."
|
|
19607
20388
|
)
|
|
@@ -19659,8 +20440,13 @@ class SessionState:
|
|
|
19659
20440
|
if is_system_key or owner == "manager":
|
|
19660
20441
|
continue
|
|
19661
20442
|
non_system_rows.append(dict(row))
|
|
19662
|
-
|
|
19663
|
-
|
|
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]
|
|
19664
20450
|
try:
|
|
19665
20451
|
todo_out = self.todo.update(merged)
|
|
19666
20452
|
except Exception:
|
|
@@ -20606,7 +21392,7 @@ class SessionState:
|
|
|
20606
21392
|
f"budget={int(self.runtime_round_budget or 0)}.\n\n"
|
|
20607
21393
|
f"{dims_ctx}"
|
|
20608
21394
|
f"{skills_ctx}"
|
|
20609
|
-
f"Workspace root: {self.files_root}\n"
|
|
21395
|
+
f"Workspace root: \"{self.files_root}\" ($SESSION_ROOT)\n"
|
|
20610
21396
|
"Infer scale_preference by semantics (fast/balanced/thorough). "
|
|
20611
21397
|
"When user preference is clear, prioritize it over your default plan. "
|
|
20612
21398
|
"Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
|
|
@@ -20815,7 +21601,8 @@ class SessionState:
|
|
|
20815
21601
|
todos = bb.get("project_todos", [])
|
|
20816
21602
|
if not todos or not any(t.get("category") == "plan_step" for t in todos):
|
|
20817
21603
|
return ""
|
|
20818
|
-
lines = ["
|
|
21604
|
+
lines = [f"PLAN FILE: {PLAN_FILE_RELATIVE_PATH} (read_file for full plan with live status)",
|
|
21605
|
+
"APPROVED PLAN STEPS:"]
|
|
20819
21606
|
for t in todos:
|
|
20820
21607
|
if t.get("category") != "plan_step":
|
|
20821
21608
|
continue
|
|
@@ -20827,10 +21614,14 @@ class SessionState:
|
|
|
20827
21614
|
lines.append(f" {mark} Step {idx}: {trim(str(t.get('content', '') or ''), 160)}{phase_tag}")
|
|
20828
21615
|
lines.append("Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing. ")
|
|
20829
21616
|
lines.append(
|
|
20830
|
-
"STEP COMPLETION RULE: Set advance_plan_step=true when
|
|
20831
|
-
"
|
|
20832
|
-
"
|
|
20833
|
-
"
|
|
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. "
|
|
20834
21625
|
)
|
|
20835
21626
|
lines.append("MANDATORY: Your delegation instruction MUST reference the current plan step. "
|
|
20836
21627
|
"Do NOT reinterpret or replace plan steps with your own objectives. ")
|
|
@@ -20859,12 +21650,15 @@ class SessionState:
|
|
|
20859
21650
|
def _plan_step_phase_hint(self, step_content: str) -> str:
|
|
20860
21651
|
"""Infer the task phase from a plan step's content."""
|
|
20861
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"
|
|
20862
21658
|
if any(kw in c for kw in ("研究", "分析", "调研", "探索", "research", "analyze", "investigate", "explore", "inspect")):
|
|
20863
21659
|
return "research"
|
|
20864
21660
|
if any(kw in c for kw in ("设计", "架构", "规划", "design", "architect", "plan", "interface", "接口")):
|
|
20865
21661
|
return "design"
|
|
20866
|
-
if any(kw in c for kw in ("实现", "编写", "创建", "开发", "implement", "write", "create", "build", "develop", "code")):
|
|
20867
|
-
return "implement"
|
|
20868
21662
|
if any(kw in c for kw in ("测试", "验证", "检查", "test", "verify", "check", "validate", "compile")):
|
|
20869
21663
|
return "test"
|
|
20870
21664
|
if any(kw in c for kw in ("审查", "评审", "review", "audit", "inspect code")):
|
|
@@ -20927,11 +21721,24 @@ class SessionState:
|
|
|
20927
21721
|
completed_w = [r for r in worker_todos if str(r.get("status", "")).lower() == "completed"]
|
|
20928
21722
|
pending_w = [r for r in worker_todos if str(r.get("status", "")).lower() not in ("completed",)]
|
|
20929
21723
|
if not pending_w and completed_w:
|
|
20930
|
-
|
|
20931
|
-
|
|
20932
|
-
|
|
20933
|
-
|
|
20934
|
-
)
|
|
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
|
+
)
|
|
20935
21742
|
elif completed_w or pending_w:
|
|
20936
21743
|
worker_hint = (
|
|
20937
21744
|
f"Worker subtasks: {len(completed_w)} done, {len(pending_w)} pending. "
|
|
@@ -22185,7 +22992,7 @@ class SessionState:
|
|
|
22185
22992
|
prompt = (
|
|
22186
22993
|
"Read the blackboard and delegate one next short timeslice. "
|
|
22187
22994
|
"Return only one route_to_next_agent call.\n\n"
|
|
22188
|
-
f"{self._blackboard_read_state_markdown(max_items=
|
|
22995
|
+
f"{self._blackboard_read_state_markdown(max_items=10)}"
|
|
22189
22996
|
)
|
|
22190
22997
|
self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
|
|
22191
22998
|
self._microcompact_agent_messages(self.manager_context)
|
|
@@ -22384,22 +23191,12 @@ class SessionState:
|
|
|
22384
23191
|
"round_budget": int(round_budget),
|
|
22385
23192
|
"remaining_rounds": int(remaining_rounds),
|
|
22386
23193
|
}
|
|
22387
|
-
# advance_plan_step:
|
|
22388
|
-
|
|
22389
|
-
#
|
|
22390
|
-
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
str(route.get("reason", "") or ""),
|
|
22394
|
-
)
|
|
22395
|
-
if should_advance:
|
|
22396
|
-
self._advance_plan_step(
|
|
22397
|
-
evidence=trim(str(route.get("instruction", "") or ""), 200),
|
|
22398
|
-
actor=str(route.get("target", "developer") or "developer"),
|
|
22399
|
-
)
|
|
22400
|
-
# CRITICAL: re-anchor board to the updated blackboard so the
|
|
22401
|
-
# self.blackboard = board at line ~22388 does NOT overwrite the advance.
|
|
22402
|
-
board = self.blackboard
|
|
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
|
|
23199
|
+
)
|
|
22403
23200
|
self.manager_routes.append(route_row)
|
|
22404
23201
|
self.manager_routes = self.manager_routes[-240:]
|
|
22405
23202
|
# Failure ledger: persist route and record delegation
|
|
@@ -22416,7 +23213,11 @@ class SessionState:
|
|
|
22416
23213
|
self.blackboard = board
|
|
22417
23214
|
self._ledger_record_delegation(target, instruction)
|
|
22418
23215
|
profile = self._ensure_blackboard_task_profile(board)
|
|
22419
|
-
|
|
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
|
|
22420
23221
|
profile["execution_mode"] = execution_mode
|
|
22421
23222
|
profile["participants"] = list(participants)
|
|
22422
23223
|
profile["assigned_expert"] = assigned_expert
|
|
@@ -22426,6 +23227,9 @@ class SessionState:
|
|
|
22426
23227
|
if task_type in TASK_PROFILE_TYPES:
|
|
22427
23228
|
profile["task_type"] = task_type
|
|
22428
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"
|
|
22429
23233
|
profile["complexity"] = complexity
|
|
22430
23234
|
profile["scale_preference"] = scale_preference if scale_preference in TASK_SCALE_PREFERENCES else "balanced"
|
|
22431
23235
|
if objective:
|
|
@@ -22633,7 +23437,7 @@ class SessionState:
|
|
|
22633
23437
|
"Do NOT repeat previous failed fixes.\n"
|
|
22634
23438
|
"</compile-error-context>\n"
|
|
22635
23439
|
)
|
|
22636
|
-
board_md = self._blackboard_read_state_markdown(max_items=
|
|
23440
|
+
board_md = self._blackboard_read_state_markdown(max_items=10)
|
|
22637
23441
|
# Include loaded skills hint in delegation
|
|
22638
23442
|
loaded_skills_note = self._loaded_skills_prompt_hint(for_role=role_key)
|
|
22639
23443
|
# Include current plan step in delegation so agents stay on track
|
|
@@ -22653,11 +23457,46 @@ class SessionState:
|
|
|
22653
23457
|
# developer 被调用时有活跃 plan step,要求在开始前调用 TodoWrite 刷新子任务
|
|
22654
23458
|
todo_update_note = ""
|
|
22655
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
|
|
22656
23467
|
todo_update_note = (
|
|
22657
|
-
"TODO UPDATE:
|
|
22658
|
-
"
|
|
22659
|
-
"
|
|
22660
|
-
|
|
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
|
|
22661
23500
|
payload = (
|
|
22662
23501
|
"<manager-delegate>\n"
|
|
22663
23502
|
f"target={role_key}\n"
|
|
@@ -22672,6 +23511,7 @@ class SessionState:
|
|
|
22672
23511
|
f"{loaded_skills_note}"
|
|
22673
23512
|
f"{current_plan_step_note}"
|
|
22674
23513
|
f"{todo_update_note}"
|
|
23514
|
+
f"{step_files_note}"
|
|
22675
23515
|
f"{error_section}"
|
|
22676
23516
|
"</manager-delegate>\n"
|
|
22677
23517
|
"<blackboard-state>\n"
|
|
@@ -22779,6 +23619,32 @@ class SessionState:
|
|
|
22779
23619
|
role_key,
|
|
22780
23620
|
f"tool_error {name}: {output}",
|
|
22781
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
|
|
22782
23648
|
|
|
22783
23649
|
def _blackboard_update_from_worker_step(self, role: str, step: dict):
|
|
22784
23650
|
role_key = self._sanitize_agent_role(role)
|
|
@@ -23391,19 +24257,32 @@ class SessionState:
|
|
|
23391
24257
|
|
|
23392
24258
|
def _agent_role_system_prompt(self, role: str) -> str:
|
|
23393
24259
|
role_key = self._sanitize_agent_role(role) or "developer"
|
|
23394
|
-
|
|
24260
|
+
skills_block = self._skills_awareness_block(for_role=role_key)
|
|
23395
24261
|
code_note = self._runtime_code_reference_prompt_block(max_chars=2600)
|
|
23396
24262
|
base = (
|
|
23397
24263
|
f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
|
|
23398
|
-
f"Workspace: {self.files_root}. Use relative paths. "
|
|
24264
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). Use relative paths or $SESSION_ROOT in bash. "
|
|
24265
|
+
f"Offline JS libraries root: $JS_LIB_ROOT. "
|
|
24266
|
+
f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
|
|
24267
|
+
f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS) or pptxgen.bundle.js (browser). "
|
|
24268
|
+
f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
|
|
23399
24269
|
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
23400
24270
|
"Keep outputs concise and action-oriented. "
|
|
23401
|
-
f"{
|
|
24271
|
+
f"{code_note + ' ' if code_note else ''}"
|
|
23402
24272
|
f"{_detect_os_shell_instruction()} "
|
|
23403
24273
|
f"{model_language_instruction(self.ui_language)} "
|
|
23404
24274
|
)
|
|
24275
|
+
mm_note = self._multimodal_capability_block()
|
|
24276
|
+
if mm_note:
|
|
24277
|
+
base = base + mm_note
|
|
24278
|
+
base = base + skills_block
|
|
23405
24279
|
if role_key == "explorer":
|
|
23406
|
-
return base +
|
|
24280
|
+
return base + (
|
|
24281
|
+
"Role: analyze goals, inspect codebase, produce research notes. "
|
|
24282
|
+
"For factual or background questions on any topic, FIRST call "
|
|
24283
|
+
"query_knowledge_library(query='<topic>', top_k=8, route='hybrid') to retrieve relevant documents. "
|
|
24284
|
+
"Prefer read/search tools. "
|
|
24285
|
+
)
|
|
23407
24286
|
if role_key == "reviewer":
|
|
23408
24287
|
if bool(self.reviewer_debug_mode):
|
|
23409
24288
|
debug_ctx = trim(str(self.reviewer_debug_context or ""), 500)
|
|
@@ -23436,6 +24315,11 @@ class SessionState:
|
|
|
23436
24315
|
)
|
|
23437
24316
|
return base + (
|
|
23438
24317
|
"Role: implement code changes, execute tools, record progress to blackboard. "
|
|
24318
|
+
"SKILL PRIORITY (critical): When ACTIVE SKILLS are listed above, find the "
|
|
24319
|
+
"<loaded-skill> messages in your context and READ them before starting any step. "
|
|
24320
|
+
"The skill's workflow, tools, and file structure OVERRIDE the plan's implementation "
|
|
24321
|
+
"approach — if the plan says 'use python-pptx' but the skill says 'use PptxGenJS', "
|
|
24322
|
+
"use PptxGenJS. The skill defines HOW to implement; the plan defines WHAT to do. "
|
|
23439
24323
|
"TODO TRACKING (mandatory): "
|
|
23440
24324
|
"After completing each logical step, call TodoWrite to update progress — "
|
|
23441
24325
|
"mark completed items as 'completed' and set the next item to 'in_progress'. "
|
|
@@ -24389,7 +25273,8 @@ class SessionState:
|
|
|
24389
25273
|
isinstance(it, dict) and str(it.get("status", it.get("state", ""))).lower() in {"completed", "done", "finished", "finish"}
|
|
24390
25274
|
for it in new_items
|
|
24391
25275
|
):
|
|
24392
|
-
self._refresh_loaded_skills_for_execution_focus(trigger="step-completed")
|
|
25276
|
+
self._refresh_loaded_skills_for_execution_focus(trigger="step-completed") # noqa: removed
|
|
25277
|
+
pass # Skills are loaded on-demand by the model
|
|
24393
25278
|
except Exception:
|
|
24394
25279
|
pass
|
|
24395
25280
|
return result
|
|
@@ -24397,7 +25282,7 @@ class SessionState:
|
|
|
24397
25282
|
result = self._todo_write_rescue(args)
|
|
24398
25283
|
# Also recheck skills on rescue write (likely a recovery situation)
|
|
24399
25284
|
try:
|
|
24400
|
-
|
|
25285
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
24401
25286
|
except Exception:
|
|
24402
25287
|
pass
|
|
24403
25288
|
return result
|
|
@@ -24848,8 +25733,6 @@ class SessionState:
|
|
|
24848
25733
|
"</live-user-adjustment>"
|
|
24849
25734
|
)
|
|
24850
25735
|
self.messages.append({"role": "user", "content": payload, "ts": now_ts()})
|
|
24851
|
-
# Merge user feedback with plan direction
|
|
24852
|
-
self._merge_user_feedback_with_plan(content)
|
|
24853
25736
|
self.runtime_reclassify_goal = trim(content, 4000)
|
|
24854
25737
|
# Only trigger reclassification in auto mode (no user override)
|
|
24855
25738
|
if int(getattr(self, 'user_task_level_override', 0) or 0) > 0:
|
|
@@ -24863,6 +25746,7 @@ class SessionState:
|
|
|
24863
25746
|
"weight": weight,
|
|
24864
25747
|
"priority": priority,
|
|
24865
25748
|
"applied": applied,
|
|
25749
|
+
"content": content,
|
|
24866
25750
|
}
|
|
24867
25751
|
)
|
|
24868
25752
|
row["applied_count"] = applied
|
|
@@ -24875,6 +25759,8 @@ class SessionState:
|
|
|
24875
25759
|
self.updated_at = now_ts()
|
|
24876
25760
|
self._persist()
|
|
24877
25761
|
for item in injected:
|
|
25762
|
+
# Merge user feedback with plan direction (outside lock — may do LLM call)
|
|
25763
|
+
self._merge_user_feedback_with_plan(item["content"])
|
|
24878
25764
|
self._emit(
|
|
24879
25765
|
"status",
|
|
24880
25766
|
{
|
|
@@ -24887,6 +25773,49 @@ class SessionState:
|
|
|
24887
25773
|
)
|
|
24888
25774
|
return len(injected)
|
|
24889
25775
|
|
|
25776
|
+
def _user_feedback_conflict_score(self, user_text: str, step_desc: str = "") -> float:
|
|
25777
|
+
"""Score 0.0–1.0 via LLM semantic analysis: how strongly user feedback conflicts with current plan.
|
|
25778
|
+
Falls back to 0.5 on error."""
|
|
25779
|
+
if not str(user_text or "").strip():
|
|
25780
|
+
return 0.0
|
|
25781
|
+
try:
|
|
25782
|
+
ctx = [
|
|
25783
|
+
{"role": "system", "content": (
|
|
25784
|
+
"You are a semantic conflict analyzer. "
|
|
25785
|
+
"Given a user's mid-execution feedback and the current task step, "
|
|
25786
|
+
"output ONLY a JSON object: {\"score\": <float 0.0-1.0>, \"reason\": \"<brief>\"}. "
|
|
25787
|
+
"score=0.0 means fully aligned (minor tweak/clarification). "
|
|
25788
|
+
"score=1.0 means direct contradiction/override (user wants opposite direction). "
|
|
25789
|
+
"No other text."
|
|
25790
|
+
), "ts": now_ts()},
|
|
25791
|
+
{"role": "user", "content": (
|
|
25792
|
+
f"Current step: {trim(step_desc, 300) or 'unknown'}\n"
|
|
25793
|
+
f"User feedback: {trim(user_text, 500)}\n"
|
|
25794
|
+
"Output JSON only."
|
|
25795
|
+
), "ts": now_ts()},
|
|
25796
|
+
]
|
|
25797
|
+
resp = self._chat_with_same_model_retry(
|
|
25798
|
+
ctx,
|
|
25799
|
+
tools=None,
|
|
25800
|
+
system=None,
|
|
25801
|
+
max_tokens=80,
|
|
25802
|
+
think=False,
|
|
25803
|
+
stream_thinking=False,
|
|
25804
|
+
context_label="feedback-conflict-score",
|
|
25805
|
+
retries=1,
|
|
25806
|
+
)
|
|
25807
|
+
text = str(resp.get("content", "") or "").strip()
|
|
25808
|
+
# Extract JSON from response
|
|
25809
|
+
import re as _re
|
|
25810
|
+
m = _re.search(r'\{[^}]+\}', text)
|
|
25811
|
+
if m:
|
|
25812
|
+
parsed = parse_json_object(m.group(0), {})
|
|
25813
|
+
score = float(parsed.get("score", 0.5) or 0.5)
|
|
25814
|
+
return max(0.0, min(1.0, score))
|
|
25815
|
+
except Exception:
|
|
25816
|
+
pass
|
|
25817
|
+
return 0.5
|
|
25818
|
+
|
|
24890
25819
|
def _merge_user_feedback_with_plan(self, user_text: str):
|
|
24891
25820
|
"""When user provides feedback during execution, inject plan-aware merge note into manager context."""
|
|
24892
25821
|
bb = self._ensure_blackboard()
|
|
@@ -24900,22 +25829,41 @@ class SessionState:
|
|
|
24900
25829
|
break
|
|
24901
25830
|
step_desc = trim(str(current_step.get("content", "") if current_step else "none"), 200)
|
|
24902
25831
|
is_plan_executing = plan.get("phase") == "executing"
|
|
25832
|
+
conflict_score = self._user_feedback_conflict_score(user_text, step_desc)
|
|
25833
|
+
if conflict_score >= 0.8:
|
|
25834
|
+
conflict_level = "HIGH"
|
|
25835
|
+
directive = (
|
|
25836
|
+
"USER OVERRIDE: This feedback DIRECTLY CONFLICTS with the current plan step. "
|
|
25837
|
+
"You MUST prioritize the user's instruction over the original plan. "
|
|
25838
|
+
"Adjust the current step's approach immediately to comply with user's requirement. "
|
|
25839
|
+
"Do NOT continue the original approach."
|
|
25840
|
+
)
|
|
25841
|
+
elif conflict_score >= 0.5:
|
|
25842
|
+
conflict_level = "MEDIUM"
|
|
25843
|
+
directive = (
|
|
25844
|
+
"This feedback modifies the current approach. "
|
|
25845
|
+
"Re-evaluate the current step and adjust delegation to incorporate user's requirement. "
|
|
25846
|
+
"User's direction takes precedence over plan details."
|
|
25847
|
+
)
|
|
25848
|
+
else:
|
|
25849
|
+
conflict_level = "LOW"
|
|
25850
|
+
directive = (
|
|
25851
|
+
"Minor feedback — integrate with current work direction. "
|
|
25852
|
+
"Adjust approach if needed but maintain progress."
|
|
25853
|
+
)
|
|
24903
25854
|
if is_plan_executing:
|
|
24904
25855
|
merge_note = (
|
|
24905
|
-
f"<user-feedback-merge>\n"
|
|
25856
|
+
f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
|
|
24906
25857
|
f"User provided new input during plan execution: {trim(user_text, 500)}\n"
|
|
24907
25858
|
f"Current plan step: {step_desc}\n"
|
|
24908
|
-
f"
|
|
24909
|
-
f"If yes, adjust delegation accordingly. If it's a new requirement, "
|
|
24910
|
-
f"integrate it into the current or next step. Do NOT restart from scratch.\n"
|
|
25859
|
+
f"{directive}\n"
|
|
24911
25860
|
f"</user-feedback-merge>"
|
|
24912
25861
|
)
|
|
24913
25862
|
else:
|
|
24914
25863
|
merge_note = (
|
|
24915
|
-
f"<user-feedback-merge>\n"
|
|
25864
|
+
f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
|
|
24916
25865
|
f"User provided new input: {trim(user_text, 500)}\n"
|
|
24917
|
-
f"
|
|
24918
|
-
f"Adjust approach if needed but maintain progress.\n"
|
|
25866
|
+
f"{directive}\n"
|
|
24919
25867
|
f"</user-feedback-merge>"
|
|
24920
25868
|
)
|
|
24921
25869
|
if self._is_multi_agent_mode():
|
|
@@ -24925,6 +25873,13 @@ class SessionState:
|
|
|
24925
25873
|
"ts": now_ts(),
|
|
24926
25874
|
"agent_role": "manager",
|
|
24927
25875
|
})
|
|
25876
|
+
else:
|
|
25877
|
+
# single mode: inject directly into message history so the model sees it
|
|
25878
|
+
self.messages.append({
|
|
25879
|
+
"role": "user",
|
|
25880
|
+
"content": merge_note,
|
|
25881
|
+
"ts": now_ts(),
|
|
25882
|
+
})
|
|
24928
25883
|
|
|
24929
25884
|
def _is_restart_scenario(self) -> bool:
|
|
24930
25885
|
"""Check if current state is a restart after finished/aborted task."""
|
|
@@ -25006,6 +25961,15 @@ class SessionState:
|
|
|
25006
25961
|
if _awaiting_plan_choice:
|
|
25007
25962
|
# Restore plan proposal so choice can be parsed
|
|
25008
25963
|
self.runtime_plan_mode_needed = True
|
|
25964
|
+
# Reset completed plan/todo/skills blackboard state so the manager
|
|
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.
|
|
25967
|
+
if not _awaiting_plan_choice:
|
|
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()
|
|
25009
25973
|
self.run_generation = int(self.run_generation) + 1
|
|
25010
25974
|
clean_goal = trim(str(content or "").strip(), 4000)
|
|
25011
25975
|
self._refresh_runtime_code_reference(clean_goal or content)
|
|
@@ -25525,6 +26489,7 @@ class SessionState:
|
|
|
25525
26489
|
if self.stall_escalation_triggered:
|
|
25526
26490
|
self._emit("status", {"summary": "sync loop break: stall escalated to plan mode"})
|
|
25527
26491
|
break
|
|
26492
|
+
self._inject_pending_user_inputs()
|
|
25528
26493
|
self._apply_auto_compact_if_needed("auto:multi-sync")
|
|
25529
26494
|
# Periodic checkpoint in multi-agent sync loop
|
|
25530
26495
|
if rounds_used % CHECKPOINT_INTERVAL_ROUNDS == 0:
|
|
@@ -25611,6 +26576,8 @@ class SessionState:
|
|
|
25611
26576
|
media_inputs_round=role_media_inputs,
|
|
25612
26577
|
)
|
|
25613
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 {})
|
|
25614
26581
|
# ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
|
|
25615
26582
|
agent_text = self._latest_agent_assistant_text(role)
|
|
25616
26583
|
if (
|
|
@@ -25890,7 +26857,7 @@ class SessionState:
|
|
|
25890
26857
|
|
|
25891
26858
|
# Auto-discover and load relevant skills before research
|
|
25892
26859
|
try:
|
|
25893
|
-
|
|
26860
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
25894
26861
|
except Exception:
|
|
25895
26862
|
pass
|
|
25896
26863
|
|
|
@@ -25900,12 +26867,22 @@ class SessionState:
|
|
|
25900
26867
|
for r in range(PLAN_MODE_EXPLORER_MAX_ROUNDS):
|
|
25901
26868
|
if self.cancel_requested:
|
|
25902
26869
|
return
|
|
26870
|
+
self._inject_pending_user_inputs()
|
|
25903
26871
|
step = self._plan_mode_explorer_turn(pinned_selection, round_idx=r)
|
|
25904
26872
|
if step.get("status") in ("no-tools", "skip", "interrupted"):
|
|
25905
26873
|
break
|
|
25906
26874
|
self._plan_mode_update_findings(step)
|
|
25907
26875
|
|
|
25908
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
|
+
|
|
25909
26886
|
self._emit("status", {"summary": "plan-mode: synthesizing proposals"})
|
|
25910
26887
|
bb = self._ensure_blackboard()
|
|
25911
26888
|
if not isinstance(bb.get("plan"), dict):
|
|
@@ -25913,7 +26890,17 @@ class SessionState:
|
|
|
25913
26890
|
bb["plan"]["phase"] = "synthesis"
|
|
25914
26891
|
self.blackboard = bb
|
|
25915
26892
|
|
|
25916
|
-
|
|
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)
|
|
25917
26904
|
if not proposal or not proposal.get("options"):
|
|
25918
26905
|
self._emit("status", {"summary": "plan-mode: synthesis failed, falling back to direct execution"})
|
|
25919
26906
|
self.runtime_plan_mode_needed = False
|
|
@@ -25929,16 +26916,23 @@ class SessionState:
|
|
|
25929
26916
|
bb["plan"]["proposal"] = proposal
|
|
25930
26917
|
self.blackboard = bb
|
|
25931
26918
|
|
|
25932
|
-
|
|
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)
|
|
25933
26927
|
self.messages.append({
|
|
25934
26928
|
"role": "assistant",
|
|
25935
|
-
"content":
|
|
26929
|
+
"content": bubble_text,
|
|
25936
26930
|
"ts": now_ts(),
|
|
25937
26931
|
"agent_role": "planner",
|
|
25938
26932
|
})
|
|
25939
26933
|
self._emit("message", {
|
|
25940
26934
|
"role": "assistant",
|
|
25941
|
-
"text": trim(
|
|
26935
|
+
"text": trim(bubble_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
|
|
25942
26936
|
"summary": "plan-mode proposal",
|
|
25943
26937
|
"agent_role": "planner",
|
|
25944
26938
|
})
|
|
@@ -25973,33 +26967,43 @@ class SessionState:
|
|
|
25973
26967
|
f"## User Request\n{goal}\n\n"
|
|
25974
26968
|
f"{skills_section}"
|
|
25975
26969
|
f"## Instructions\n"
|
|
25976
|
-
f"1.
|
|
25977
|
-
f"
|
|
25978
|
-
f"
|
|
25979
|
-
f"
|
|
25980
|
-
f"4.
|
|
25981
|
-
f"
|
|
25982
|
-
f"
|
|
25983
|
-
f"
|
|
26970
|
+
f"1. Call `list_skills` FIRST to discover available skills — identify which skills are relevant "
|
|
26971
|
+
f"to this task and note their names and capabilities in your findings.\n"
|
|
26972
|
+
f"2. List all uploaded/workspace files with `ls uploaded/` or `ls` to know what inputs are available\n"
|
|
26973
|
+
f"3. Read uploaded files (.parsed.md preferred over .pdf) to understand their content and structure\n"
|
|
26974
|
+
f"4. If relevant skills exist, call `load_skill` to load the most relevant one and analyze its "
|
|
26975
|
+
f"workflow steps, scripts, tools, and file paths\n"
|
|
26976
|
+
f"5. Identify key technical details, data points, and structure needed for the output\n"
|
|
26977
|
+
f"6. Assess risks and note any ambiguities that need user input\n"
|
|
26978
|
+
f"7. DO NOT write, edit, or create any files. Read-only analysis only.\n"
|
|
26979
|
+
f"8. Write your findings to the blackboard under 'plan_findings'. Include:\n"
|
|
26980
|
+
f" - Relevant skills found (names, what they do, how to invoke them)\n"
|
|
25984
26981
|
f" - File inventory (uploaded files, their types, sizes, key content)\n"
|
|
25985
|
-
f" - Skill workflow breakdown (concrete tools, scripts, paths for each
|
|
25986
|
-
f" - Content analysis (key themes, structure, data points extracted from inputs)\n
|
|
25987
|
-
f"
|
|
26982
|
+
f" - Skill workflow breakdown (concrete tools, scripts, paths for each relevant skill)\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"
|
|
26989
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT)\n"
|
|
25988
26990
|
f"{os_note}\n"
|
|
25989
26991
|
f"{lang_note}"
|
|
25990
26992
|
)
|
|
25991
26993
|
|
|
25992
26994
|
def _seed_plan_mode_explorer_context(self, research_prompt: str):
|
|
25993
26995
|
os_note = _detect_os_shell_instruction()
|
|
25994
|
-
|
|
26996
|
+
skills_block = self._skills_awareness_block(for_role="explorer")
|
|
25995
26997
|
self._append_agent_context_message("explorer", {
|
|
25996
26998
|
"role": "system",
|
|
25997
26999
|
"content": (
|
|
25998
27000
|
"You are Explorer in plan-mode (read-only research). "
|
|
25999
27001
|
"Analyze the codebase to understand the task scope. "
|
|
26000
27002
|
"Do NOT modify any files. Use read_file, bash (read-only commands), "
|
|
26001
|
-
"and blackboard tools only. "
|
|
26002
|
-
f"{
|
|
27003
|
+
"list_skills, load_skill, and blackboard tools only. "
|
|
27004
|
+
f"{skills_block}"
|
|
27005
|
+
"IMPORTANT: If the task requires specialized output (PPTX, reports, deep research, code review), "
|
|
27006
|
+
"call list_skills first to discover relevant skills, then note in plan_findings which skills to use. "
|
|
26003
27007
|
f"{os_note} "
|
|
26004
27008
|
f"{model_language_instruction(self.ui_language)}"
|
|
26005
27009
|
),
|
|
@@ -26030,16 +27034,16 @@ class SessionState:
|
|
|
26030
27034
|
self.current_phase = f"plan-mode:explorer:round-{round_idx}"
|
|
26031
27035
|
self.current_tool_name = ""
|
|
26032
27036
|
self.active_agent_role = "explorer"
|
|
26033
|
-
# Build
|
|
26034
|
-
|
|
27037
|
+
# Build skills awareness block (same as sync/single mode)
|
|
27038
|
+
skills_block = self._skills_awareness_block(for_role="explorer")
|
|
26035
27039
|
response = self._chat_with_same_model_retry(
|
|
26036
27040
|
ctx,
|
|
26037
27041
|
tools=filtered_tools,
|
|
26038
27042
|
system=(
|
|
26039
27043
|
"You are Explorer in plan-mode research. Read-only analysis. "
|
|
26040
27044
|
"Do NOT create, write, or edit files. "
|
|
26041
|
-
f"Workspace: {self.files_root}. "
|
|
26042
|
-
f"{
|
|
27045
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
|
|
27046
|
+
f"{skills_block}"
|
|
26043
27047
|
f"{_detect_os_shell_instruction()} "
|
|
26044
27048
|
f"{model_language_instruction(self.ui_language)}"
|
|
26045
27049
|
),
|
|
@@ -26443,12 +27447,22 @@ class SessionState:
|
|
|
26443
27447
|
f"- Each step should be completable in 1-3 tool calls\n"
|
|
26444
27448
|
f"- Group related substeps under numbered headings (e.g., '2.1 Read report 1', '2.2 Read report 2')\n"
|
|
26445
27449
|
f"Make options meaningfully different (e.g. different approaches, scope levels, or trade-offs).\n"
|
|
26446
|
-
|
|
26447
|
-
|
|
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)}"
|
|
26448
27461
|
synthesis_ctx = [
|
|
26449
27462
|
{"role": "system", "content": (
|
|
26450
27463
|
"You are a technical architect synthesizing research into actionable plans. "
|
|
26451
|
-
"When
|
|
27464
|
+
"When skills are referenced in the findings, incorporate their actual workflow steps into plan options. "
|
|
27465
|
+
f"{self._skills_awareness_block(for_role='developer')}"
|
|
26452
27466
|
), "ts": now_ts()},
|
|
26453
27467
|
{"role": "user", "content": synthesis_prompt, "ts": now_ts()},
|
|
26454
27468
|
]
|
|
@@ -26472,6 +27486,63 @@ class SessionState:
|
|
|
26472
27486
|
return dict(args)
|
|
26473
27487
|
return {}
|
|
26474
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
|
+
|
|
26475
27546
|
def _plan_mode_synthesis_tools(self) -> list:
|
|
26476
27547
|
return [tool_def(
|
|
26477
27548
|
"submit_plan_proposal",
|
|
@@ -26499,6 +27570,185 @@ class SessionState:
|
|
|
26499
27570
|
["context", "options", "recommended"],
|
|
26500
27571
|
)]
|
|
26501
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
|
+
|
|
26502
27752
|
def _format_plan_proposal_markdown(self, proposal: dict) -> str:
|
|
26503
27753
|
lines = ["## 📋 执行方案\n"]
|
|
26504
27754
|
context = str(proposal.get("context", "") or "").strip()
|
|
@@ -26624,16 +27874,9 @@ class SessionState:
|
|
|
26624
27874
|
)
|
|
26625
27875
|
if not chosen:
|
|
26626
27876
|
return
|
|
26627
|
-
|
|
26628
|
-
|
|
26629
|
-
|
|
26630
|
-
f"## {chosen.get('title', '')}\n"
|
|
26631
|
-
f"{chosen.get('summary', '')}\n\n"
|
|
26632
|
-
f"### Steps:\n{steps_text}\n\n"
|
|
26633
|
-
f"Follow these steps. Use tools to implement each step concretely.\n"
|
|
26634
|
-
f"If a step references a skill or workflow you don't fully understand, "
|
|
26635
|
-
f"call load_skill to load it and read its instructions before proceeding."
|
|
26636
|
-
)
|
|
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()
|
|
26637
27880
|
self.messages.append({
|
|
26638
27881
|
"role": "system",
|
|
26639
27882
|
"content": plan_msg,
|
|
@@ -26651,6 +27894,9 @@ class SessionState:
|
|
|
26651
27894
|
bb["plan"] = {"phase": "executing", "chosen": choice_id, "steps": chosen.get("steps", [])}
|
|
26652
27895
|
self.blackboard = bb
|
|
26653
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)
|
|
26654
27900
|
# Auto-create todos from plan steps → write into bb["project_todos"]
|
|
26655
27901
|
steps = chosen.get("steps", [])
|
|
26656
27902
|
if steps and isinstance(steps, list):
|
|
@@ -26671,7 +27917,7 @@ class SessionState:
|
|
|
26671
27917
|
"evidence": "",
|
|
26672
27918
|
})
|
|
26673
27919
|
if plan_todos:
|
|
26674
|
-
bb["project_todos"] = plan_todos[:
|
|
27920
|
+
bb["project_todos"] = plan_todos[:40]
|
|
26675
27921
|
bb["plan_step_cursor"] = 0
|
|
26676
27922
|
bb["plan_step_total"] = len(plan_todos)
|
|
26677
27923
|
self.blackboard = bb
|
|
@@ -26686,12 +27932,13 @@ class SessionState:
|
|
|
26686
27932
|
"status": t["status"],
|
|
26687
27933
|
"activeForm": f"Working on: {t['content']}" if t["status"] == "in_progress" else f"Pending: {t['content']}",
|
|
26688
27934
|
}
|
|
26689
|
-
for t in plan_todos[:
|
|
27935
|
+
for t in plan_todos[:40]
|
|
26690
27936
|
])
|
|
26691
27937
|
except Exception:
|
|
26692
27938
|
pass
|
|
27939
|
+
# Write execution-phase plan file (now with todos populated)
|
|
26693
27940
|
try:
|
|
26694
|
-
self.
|
|
27941
|
+
self._write_plan_file(self._format_plan_file_execution(choice_id))
|
|
26695
27942
|
except Exception:
|
|
26696
27943
|
pass
|
|
26697
27944
|
# Pre-load skills explicitly mentioned in plan steps
|
|
@@ -26755,13 +28002,13 @@ class SessionState:
|
|
|
26755
28002
|
)
|
|
26756
28003
|
},
|
|
26757
28004
|
)
|
|
26758
|
-
# ──
|
|
28005
|
+
# ── Skills are loaded on-demand by the model via load_skill ──
|
|
26759
28006
|
try:
|
|
26760
28007
|
self._emit(
|
|
26761
28008
|
"status",
|
|
26762
|
-
{"summary": "
|
|
28009
|
+
{"summary": "skills available on-demand"},
|
|
26763
28010
|
)
|
|
26764
|
-
|
|
28011
|
+
pass # No automatic pre-classify skill discovery
|
|
26765
28012
|
except Exception:
|
|
26766
28013
|
pass
|
|
26767
28014
|
initial_policy_media_inputs = self._recent_multimodal_inputs()
|
|
@@ -29781,6 +31028,9 @@ h3{font-size:.96rem;margin:10px 0 6px}
|
|
|
29781
31028
|
.todo-item,.task-item{border:1px solid #e4ebf4;border-left-width:4px;border-radius:10px;padding:8px 10px;background:#fcfdff}
|
|
29782
31029
|
.todo-item.st-pending,.task-item.st-pending{border-left-color:#7b8798}
|
|
29783
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}
|
|
29784
31034
|
.todo-item.st-completed,.task-item.st-completed{border-left-color:#13b8a6;background:#edfcf7}
|
|
29785
31035
|
.todo-item.st-blocked,.task-item.st-blocked{border-left-color:#b96b00;background:#fff6ea}
|
|
29786
31036
|
.todo-item.st-deleted,.task-item.st-deleted{border-left-color:#a0a6b0;background:#f7f8fa}
|
|
@@ -29939,7 +31189,8 @@ const I18N={
|
|
|
29939
31189
|
llm_fill_config:'Fill LLM Config',llm_provider:'Provider',llm_confirm:'Confirm',llm_import_config:'Import config',
|
|
29940
31190
|
llm_thinking_stream:'Thinking Stream',llm_enabled:'Enabled',llm_disabled:'Disabled',
|
|
29941
31191
|
llm_model:'Model',llm_scan:'Scan',llm_scan_hint:'Click Scan to detect models from Ollama',llm_scan_first:'Scan models first',
|
|
29942
|
-
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'
|
|
29943
31194
|
},
|
|
29944
31195
|
'zh-CN':{
|
|
29945
31196
|
app_title:'Clouds Coder',app_subtitle:'WebUI 驱动的会话式集成编程 Agent 平台',powered_by:'Powered By Fona',
|
|
@@ -29974,7 +31225,8 @@ const I18N={
|
|
|
29974
31225
|
llm_fill_config:'填写 LLM 配置',llm_provider:'供应商',llm_confirm:'确认',llm_import_config:'导入配置',
|
|
29975
31226
|
llm_thinking_stream:'思维流',llm_enabled:'启用',llm_disabled:'禁用',
|
|
29976
31227
|
llm_model:'模型',llm_scan:'扫描',llm_scan_hint:'点击扫描检测 Ollama 可用模型',llm_scan_first:'请先扫描模型',
|
|
29977
|
-
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:'子任务'
|
|
29978
31230
|
},
|
|
29979
31231
|
'zh-TW':{
|
|
29980
31232
|
app_title:'Clouds Coder',app_subtitle:'WebUI 驅動的會話式整合程式 Agent 平台',powered_by:'Powered By Fona',
|
|
@@ -30009,7 +31261,8 @@ const I18N={
|
|
|
30009
31261
|
llm_fill_config:'填寫 LLM 設定',llm_provider:'供應商',llm_confirm:'確認',llm_import_config:'匯入設定',
|
|
30010
31262
|
llm_thinking_stream:'思維流',llm_enabled:'啟用',llm_disabled:'停用',
|
|
30011
31263
|
llm_model:'模型',llm_scan:'掃描',llm_scan_hint:'點擊掃描偵測 Ollama 可用模型',llm_scan_first:'請先掃描模型',
|
|
30012
|
-
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:'子任務'
|
|
30013
31266
|
},
|
|
30014
31267
|
'ja':{
|
|
30015
31268
|
app_title:'Clouds Coder',app_subtitle:'WebUI 駆動の対話型コーディング Agent プラットフォーム',powered_by:'Powered By Fona',
|
|
@@ -30044,7 +31297,8 @@ const I18N={
|
|
|
30044
31297
|
llm_fill_config:'LLM設定入力',llm_provider:'プロバイダー',llm_confirm:'確認',llm_import_config:'設定をインポート',
|
|
30045
31298
|
llm_thinking_stream:'シンキングストリーム',llm_enabled:'有効',llm_disabled:'無効',
|
|
30046
31299
|
llm_model:'モデル',llm_scan:'スキャン',llm_scan_hint:'スキャンをクリックしてOllamaモデルを検出',llm_scan_first:'先にモデルをスキャン',
|
|
30047
|
-
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:'サブタスク'
|
|
30048
31302
|
}
|
|
30049
31303
|
};
|
|
30050
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'}
|
|
@@ -30581,16 +31835,32 @@ function _mathRunTypeset(root,key=''){
|
|
|
30581
31835
|
if(!root)return;
|
|
30582
31836
|
const k=String(key||'').trim();
|
|
30583
31837
|
if(k&&root.getAttribute('data-math-key')===k)return;
|
|
31838
|
+
const mathJaxCandidates=[
|
|
31839
|
+
'/assets/js_lib/tex-mml-chtml.js',
|
|
31840
|
+
'/assets/js_lib/mathjax/tex-mml-chtml.js',
|
|
31841
|
+
'/assets/js_lib/es5/tex-mml-chtml.js',
|
|
31842
|
+
'/assets/js_lib/mathjax/es5/tex-mml-chtml.js',
|
|
31843
|
+
'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
|
|
31844
|
+
];
|
|
31845
|
+
const loadMathJax=(idx=0)=>{
|
|
31846
|
+
const src=String(mathJaxCandidates[idx]||'').trim();
|
|
31847
|
+
if(!src)return;
|
|
31848
|
+
const s=document.createElement('script');
|
|
31849
|
+
s.src=src;
|
|
31850
|
+
s.async=true;
|
|
31851
|
+
s.dataset.mathjaxCandidate=String(idx);
|
|
31852
|
+
s.onerror=()=>{
|
|
31853
|
+
if(idx+1<mathJaxCandidates.length)loadMathJax(idx+1);
|
|
31854
|
+
};
|
|
31855
|
+
document.head.appendChild(s);
|
|
31856
|
+
};
|
|
30584
31857
|
const run=(retry)=>{
|
|
30585
31858
|
const mj=window.MathJax;
|
|
30586
31859
|
if(!mj||typeof mj.typesetPromise!=='function'){
|
|
30587
31860
|
// Lazy-load MathJax on first actual math demand
|
|
30588
31861
|
if(!window._mjaxLoading){
|
|
30589
31862
|
window._mjaxLoading=true;
|
|
30590
|
-
|
|
30591
|
-
s.src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
|
|
30592
|
-
s.async=true;
|
|
30593
|
-
document.head.appendChild(s);
|
|
31863
|
+
loadMathJax(0);
|
|
30594
31864
|
}
|
|
30595
31865
|
if(retry<20)setTimeout(()=>run(retry+1),200);
|
|
30596
31866
|
return;
|
|
@@ -32339,7 +33609,54 @@ function statusClass(status){return `st-${normalizeStatus(status)}`}
|
|
|
32339
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')}
|
|
32340
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()}
|
|
32341
33611
|
function formatTs(ts){const v=Number(ts||0);if(!v)return '';try{return new Date(v*1000).toLocaleString()}catch(_){return ''}}
|
|
32342
|
-
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
|
+
}
|
|
32343
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>`}
|
|
32344
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}
|
|
32345
33662
|
function _fePath(sessionId){const sid=encodeURIComponent(String(sessionId||'').trim());return `/api/sessions/${sid}/files-tree`}
|
|
@@ -34349,7 +35666,7 @@ class RAGContentParser:
|
|
|
34349
35666
|
|
|
34350
35667
|
text = extract_text(str(pdf_path))
|
|
34351
35668
|
if text and text.strip():
|
|
34352
|
-
return trim(text.strip(),
|
|
35669
|
+
return trim(text.strip(), 800_000)
|
|
34353
35670
|
except ImportError:
|
|
34354
35671
|
pass
|
|
34355
35672
|
except Exception:
|
|
@@ -34364,7 +35681,7 @@ class RAGContentParser:
|
|
|
34364
35681
|
timeout=60,
|
|
34365
35682
|
)
|
|
34366
35683
|
if r.returncode == 0 and r.stdout.strip():
|
|
34367
|
-
return trim(r.stdout.strip(),
|
|
35684
|
+
return trim(r.stdout.strip(), 800_000)
|
|
34368
35685
|
except Exception:
|
|
34369
35686
|
pass
|
|
34370
35687
|
try:
|
|
@@ -36525,7 +37842,7 @@ class RAGLibraryStore:
|
|
|
36525
37842
|
backup_path.write_bytes(raw_bytes)
|
|
36526
37843
|
elif source_fp and source_fp.exists():
|
|
36527
37844
|
shutil.copy2(source_fp, backup_path)
|
|
36528
|
-
semantic_text = trim(str(parse_result.get("text", "") or ""),
|
|
37845
|
+
semantic_text = trim(str(parse_result.get("text", "") or ""), 800_000)
|
|
36529
37846
|
multimodal_row = dict(multimodal or {})
|
|
36530
37847
|
mm_summary = trim(str(multimodal_row.get("summary", "") or ""), 2400)
|
|
36531
37848
|
mm_tags = [str(x).strip() for x in (multimodal_row.get("tags", []) or []) if str(x).strip()]
|
|
@@ -39952,20 +41269,12 @@ class AppContext:
|
|
|
39952
41269
|
self.base_url = base_url
|
|
39953
41270
|
self.model = model
|
|
39954
41271
|
self.thinking = False
|
|
39955
|
-
self.js_lib_root = offline_js_lib_root(
|
|
41272
|
+
self.js_lib_root = offline_js_lib_root(SCRIPT_DIR)
|
|
39956
41273
|
self.offline_js_summary: dict = {}
|
|
39957
41274
|
try:
|
|
39958
|
-
self.offline_js_summary =
|
|
39959
|
-
except Exception
|
|
39960
|
-
self.offline_js_summary = {
|
|
39961
|
-
"generated_at": int(now_ts()),
|
|
39962
|
-
"js_lib_root": str(self.js_lib_root),
|
|
39963
|
-
"total": len(OFFLINE_JS_LIB_CATALOG),
|
|
39964
|
-
"available": 0,
|
|
39965
|
-
"missing": len(OFFLINE_JS_LIB_CATALOG),
|
|
39966
|
-
"fetched": 0,
|
|
39967
|
-
"error": trim(str(exc), 220),
|
|
39968
|
-
}
|
|
41275
|
+
self.offline_js_summary = load_offline_js_lib_index(self.js_lib_root)
|
|
41276
|
+
except Exception:
|
|
41277
|
+
self.offline_js_summary = {}
|
|
39969
41278
|
self.default_language = normalize_ui_language(default_language)
|
|
39970
41279
|
self.ui_style = normalize_ui_style(ui_style)
|
|
39971
41280
|
self.context_token_limit = max(
|
|
@@ -40264,16 +41573,7 @@ class AppContext:
|
|
|
40264
41573
|
return CODE_ADMIN_JS
|
|
40265
41574
|
|
|
40266
41575
|
def rag_js_lib_asset_path(self, filename: str) -> Path | None:
|
|
40267
|
-
|
|
40268
|
-
fp = (self.js_lib_root / safe).resolve()
|
|
40269
|
-
try:
|
|
40270
|
-
if not fp.is_relative_to(self.js_lib_root):
|
|
40271
|
-
return None
|
|
40272
|
-
except Exception:
|
|
40273
|
-
return None
|
|
40274
|
-
if not fp.exists() or (not fp.is_file()):
|
|
40275
|
-
return None
|
|
40276
|
-
return fp
|
|
41576
|
+
return _resolve_js_lib_asset_path(self.js_lib_root, str(filename or "").strip())
|
|
40277
41577
|
|
|
40278
41578
|
def rag_three_asset_info(self) -> dict:
|
|
40279
41579
|
picks = [
|
|
@@ -41071,7 +42371,7 @@ class AppContext:
|
|
|
41071
42371
|
f"{str(row.get('title', '') or '').strip()} "
|
|
41072
42372
|
f"score={str(row.get('score', 0) or 0)}"
|
|
41073
42373
|
)
|
|
41074
|
-
snippet = trim(str(row.get("text", "") or ""),
|
|
42374
|
+
snippet = trim(str(row.get("text", "") or ""), 800)
|
|
41075
42375
|
if snippet:
|
|
41076
42376
|
lines.append(snippet)
|
|
41077
42377
|
return "\n".join(lines)
|
|
@@ -43150,8 +44450,8 @@ class RagAdminHandler(BaseHTTPRequestHandler):
|
|
|
43150
44450
|
if path == "/assets/rag-admin.js":
|
|
43151
44451
|
return self._send_text(self.app.web_ui_rag_admin_js(), "application/javascript; charset=utf-8")
|
|
43152
44452
|
if path.startswith("/assets/js_lib/"):
|
|
43153
|
-
|
|
43154
|
-
fp = self.app.rag_js_lib_asset_path(
|
|
44453
|
+
asset_ref = path[len("/assets/js_lib/"):]
|
|
44454
|
+
fp = self.app.rag_js_lib_asset_path(asset_ref)
|
|
43155
44455
|
if not fp:
|
|
43156
44456
|
return self._send_json({"error": "asset not found"}, status=404)
|
|
43157
44457
|
try:
|
|
@@ -43159,7 +44459,7 @@ class RagAdminHandler(BaseHTTPRequestHandler):
|
|
|
43159
44459
|
except Exception as exc:
|
|
43160
44460
|
return self._send_json({"error": str(exc)}, status=500)
|
|
43161
44461
|
content_type = guess_mime_from_name(fp.name, "application/javascript")
|
|
43162
|
-
if fp.suffix.lower()
|
|
44462
|
+
if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
|
|
43163
44463
|
content_type = "application/javascript; charset=utf-8"
|
|
43164
44464
|
return self._send_inline_bytes(data, content_type)
|
|
43165
44465
|
if path == "/api/health":
|
|
@@ -43307,8 +44607,8 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
|
|
|
43307
44607
|
if path == "/assets/code-admin.js":
|
|
43308
44608
|
return self._send_text(self.app.web_ui_code_admin_js(), "application/javascript; charset=utf-8")
|
|
43309
44609
|
if path.startswith("/assets/js_lib/"):
|
|
43310
|
-
|
|
43311
|
-
fp = self.app.rag_js_lib_asset_path(
|
|
44610
|
+
asset_ref = path[len("/assets/js_lib/"):]
|
|
44611
|
+
fp = self.app.rag_js_lib_asset_path(asset_ref)
|
|
43312
44612
|
if not fp:
|
|
43313
44613
|
return self._send_json({"error": "asset not found"}, status=404)
|
|
43314
44614
|
try:
|
|
@@ -43316,7 +44616,7 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
|
|
|
43316
44616
|
except Exception as exc:
|
|
43317
44617
|
return self._send_json({"error": str(exc)}, status=500)
|
|
43318
44618
|
content_type = guess_mime_from_name(fp.name, "application/javascript")
|
|
43319
|
-
if fp.suffix.lower()
|
|
44619
|
+
if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
|
|
43320
44620
|
content_type = "application/javascript; charset=utf-8"
|
|
43321
44621
|
return self._send_inline_bytes(data, content_type)
|
|
43322
44622
|
if path == "/api/health":
|
|
@@ -43938,6 +45238,17 @@ def main():
|
|
|
43938
45238
|
except Exception as exc:
|
|
43939
45239
|
print(f"[web-agent] failed to apply --config: {exc}")
|
|
43940
45240
|
sys.exit(2)
|
|
45241
|
+
# JS lib download (default on; set download_js_lib: false in --config to disable)
|
|
45242
|
+
_js_dl_enabled = extract_js_lib_download_setting(external_config)
|
|
45243
|
+
if _js_dl_enabled is None:
|
|
45244
|
+
_js_dl_enabled = True
|
|
45245
|
+
if _js_dl_enabled:
|
|
45246
|
+
try:
|
|
45247
|
+
app.offline_js_summary = ensure_offline_js_libs(
|
|
45248
|
+
app.workspace, force=False, verbose=True, no_connection_deadline=60.0
|
|
45249
|
+
)
|
|
45250
|
+
except Exception as _js_exc:
|
|
45251
|
+
print(f"[js_lib] download error: {_js_exc}")
|
|
43941
45252
|
web_ui_state = app.configure_web_ui(
|
|
43942
45253
|
config_path=str(web_ui_config_path),
|
|
43943
45254
|
ui_dir=resolved_web_ui_dir,
|