clouds-coder 2026.3.25__tar.gz → 2026.3.27__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.27}/Clouds_Coder.py +867 -131
- {clouds_coder-2026.3.25/clouds_coder.egg-info → clouds_coder-2026.3.27}/PKG-INFO +25 -3
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/README.md +24 -2
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27/clouds_coder.egg-info}/PKG-INFO +25 -3
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/pyproject.toml +1 -1
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/LICENSE +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/setup.cfg +0 -0
- {clouds_coder-2026.3.25 → clouds_coder-2026.3.27}/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
|
|
@@ -974,11 +975,214 @@ OFFLINE_JS_LIB_CATALOG: list[dict[str, object]] = [
|
|
|
974
975
|
],
|
|
975
976
|
"match_tokens": ["cdn.tailwindcss.com", "@tailwindcss/browser", "tailwindcss"],
|
|
976
977
|
},
|
|
978
|
+
{
|
|
979
|
+
"id": "mathjax",
|
|
980
|
+
"filename": "tex-mml-chtml.js",
|
|
981
|
+
"relative_path": "mathjax/es5/tex-mml-chtml.js",
|
|
982
|
+
"package_urls": [
|
|
983
|
+
"https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz",
|
|
984
|
+
],
|
|
985
|
+
"package_install_dir": "mathjax",
|
|
986
|
+
"package_required_paths": [
|
|
987
|
+
"package.json",
|
|
988
|
+
"es5/core.js",
|
|
989
|
+
"es5/loader.js",
|
|
990
|
+
"es5/startup.js",
|
|
991
|
+
"es5/tex-mml-chtml.js",
|
|
992
|
+
],
|
|
993
|
+
"match_tokens": ["mathjax", "tex-mml-chtml.js", "/mathjax@"],
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
"id": "katex",
|
|
997
|
+
"filename": "katex.min.js",
|
|
998
|
+
"relative_path": "katex/dist/katex.min.js",
|
|
999
|
+
"package_urls": [
|
|
1000
|
+
"https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
|
|
1001
|
+
],
|
|
1002
|
+
"package_install_dir": "katex",
|
|
1003
|
+
"package_required_paths": [
|
|
1004
|
+
"package.json",
|
|
1005
|
+
"dist/katex.min.js",
|
|
1006
|
+
"dist/katex.min.css",
|
|
1007
|
+
"dist/contrib/auto-render.min.js",
|
|
1008
|
+
],
|
|
1009
|
+
"match_tokens": ["katex", "katex.min.js", "/katex@"],
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"id": "katex_auto_render",
|
|
1013
|
+
"filename": "auto-render.min.js",
|
|
1014
|
+
"relative_path": "katex/dist/contrib/auto-render.min.js",
|
|
1015
|
+
"match_tokens": ["auto-render.min.js", "katex/contrib/auto-render", "katex-auto-render"],
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"id": "html2canvas",
|
|
1019
|
+
"filename": "html2canvas.min.js",
|
|
1020
|
+
"urls": [
|
|
1021
|
+
"https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js",
|
|
1022
|
+
"https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js",
|
|
1023
|
+
],
|
|
1024
|
+
"match_tokens": ["html2canvas", "html2canvas.min.js"],
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
"id": "jspdf",
|
|
1028
|
+
"filename": "jspdf.umd.min.js",
|
|
1029
|
+
"urls": [
|
|
1030
|
+
"https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js",
|
|
1031
|
+
"https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js",
|
|
1032
|
+
],
|
|
1033
|
+
"match_tokens": ["jspdf", "jspdf.umd.min.js"],
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
"id": "xlsx",
|
|
1037
|
+
"filename": "xlsx.full.min.js",
|
|
1038
|
+
"urls": [
|
|
1039
|
+
"https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js",
|
|
1040
|
+
"https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js",
|
|
1041
|
+
],
|
|
1042
|
+
"match_tokens": ["xlsx", "xlsx.full.min.js", "sheetjs"],
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
"id": "jszip",
|
|
1046
|
+
"filename": "jszip.min.js",
|
|
1047
|
+
"relative_path": "node_modules/jszip/dist/jszip.min.js",
|
|
1048
|
+
"package_urls": [
|
|
1049
|
+
"https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
|
1050
|
+
],
|
|
1051
|
+
"package_install_dir": "node_modules/jszip",
|
|
1052
|
+
"package_required_paths": [
|
|
1053
|
+
"package.json",
|
|
1054
|
+
"dist/jszip.min.js",
|
|
1055
|
+
],
|
|
1056
|
+
"package_postprocess": "jszip-main-to-dist",
|
|
1057
|
+
"match_tokens": ["jszip", "jszip.min.js"],
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"id": "pptxgenjs",
|
|
1061
|
+
"filename": "pptxgen.bundle.js",
|
|
1062
|
+
"relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
|
|
1063
|
+
"package_urls": [
|
|
1064
|
+
"https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
|
|
1065
|
+
],
|
|
1066
|
+
"package_install_dir": "pptxgenjs",
|
|
1067
|
+
"package_required_paths": [
|
|
1068
|
+
"package.json",
|
|
1069
|
+
"dist/pptxgen.bundle.js",
|
|
1070
|
+
"dist/pptxgen.cjs.js",
|
|
1071
|
+
"dist/pptxgen.es.js",
|
|
1072
|
+
"dist/pptxgen.min.js",
|
|
1073
|
+
],
|
|
1074
|
+
"match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.cjs.js", "pptxgen.es.js", "pptxgen.min.js", "jszip.min.js"],
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"id": "pptxgenjs_bundle",
|
|
1078
|
+
"filename": "pptxgen.bundle.js",
|
|
1079
|
+
"relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
|
|
1080
|
+
"match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.bundle"],
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
"id": "pptxgenjs_cjs",
|
|
1084
|
+
"filename": "pptxgen.cjs.js",
|
|
1085
|
+
"relative_path": "pptxgenjs/dist/pptxgen.cjs.js",
|
|
1086
|
+
"match_tokens": ["pptxgenjs", "pptxgen.cjs.js", "pptxgen.cjs"],
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"id": "pptxgenjs_es",
|
|
1090
|
+
"filename": "pptxgen.es.js",
|
|
1091
|
+
"relative_path": "pptxgenjs/dist/pptxgen.es.js",
|
|
1092
|
+
"match_tokens": ["pptxgenjs", "pptxgen.es.js", "pptxgen.es"],
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
"id": "pptxgenjs_min",
|
|
1096
|
+
"filename": "pptxgen.min.js",
|
|
1097
|
+
"relative_path": "pptxgenjs/dist/pptxgen.min.js",
|
|
1098
|
+
"match_tokens": ["pptxgenjs", "pptxgen.min.js", "pptxgen.min"],
|
|
1099
|
+
},
|
|
977
1100
|
]
|
|
978
1101
|
OFFLINE_JS_LIB_INDEX_FILE = "index.json"
|
|
979
1102
|
OFFLINE_JS_LIB_README_FILE = "README.md"
|
|
980
1103
|
|
|
981
1104
|
|
|
1105
|
+
def _normalize_js_lib_asset_ref(value: str) -> str:
|
|
1106
|
+
raw = str(value or "").replace("\\", "/").strip()
|
|
1107
|
+
if not raw:
|
|
1108
|
+
return ""
|
|
1109
|
+
raw = raw.lstrip("/")
|
|
1110
|
+
pure = PurePosixPath(raw)
|
|
1111
|
+
parts: list[str] = []
|
|
1112
|
+
for part in pure.parts:
|
|
1113
|
+
if part in {"", "."}:
|
|
1114
|
+
continue
|
|
1115
|
+
if part == "..":
|
|
1116
|
+
return ""
|
|
1117
|
+
parts.append(part)
|
|
1118
|
+
return "/".join(parts)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _resolve_js_lib_asset_path(js_root: Path, asset_ref: str) -> Path | None:
|
|
1122
|
+
rel = _normalize_js_lib_asset_ref(asset_ref)
|
|
1123
|
+
if not rel:
|
|
1124
|
+
return None
|
|
1125
|
+
exact = (js_root / rel).resolve()
|
|
1126
|
+
try:
|
|
1127
|
+
if exact.is_file() and exact.is_relative_to(js_root):
|
|
1128
|
+
return exact
|
|
1129
|
+
except Exception:
|
|
1130
|
+
pass
|
|
1131
|
+
basename = _safe_js_filename(Path(rel).name, "lib.js")
|
|
1132
|
+
candidates: list[Path] = []
|
|
1133
|
+
try:
|
|
1134
|
+
for fp in js_root.rglob(basename):
|
|
1135
|
+
try:
|
|
1136
|
+
resolved = fp.resolve()
|
|
1137
|
+
except Exception:
|
|
1138
|
+
continue
|
|
1139
|
+
if resolved.is_file():
|
|
1140
|
+
try:
|
|
1141
|
+
if resolved.is_relative_to(js_root):
|
|
1142
|
+
candidates.append(resolved)
|
|
1143
|
+
except Exception:
|
|
1144
|
+
continue
|
|
1145
|
+
except Exception:
|
|
1146
|
+
return None
|
|
1147
|
+
if not candidates:
|
|
1148
|
+
return None
|
|
1149
|
+
candidates.sort(key=lambda p: (len(p.relative_to(js_root).parts), p.relative_to(js_root).as_posix()))
|
|
1150
|
+
return candidates[0]
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def _discover_extra_js_lib_files(js_root: Path, known_relative_paths: set[str]) -> list[dict]:
|
|
1154
|
+
rows: list[dict] = []
|
|
1155
|
+
if not js_root.exists():
|
|
1156
|
+
return rows
|
|
1157
|
+
seen: set[str] = set()
|
|
1158
|
+
for fp in sorted(js_root.rglob("*")):
|
|
1159
|
+
if not fp.is_file() or fp.name in {".DS_Store", OFFLINE_JS_LIB_INDEX_FILE, OFFLINE_JS_LIB_README_FILE}:
|
|
1160
|
+
continue
|
|
1161
|
+
if fp.suffix.lower() not in {".js", ".mjs", ".cjs"}:
|
|
1162
|
+
continue
|
|
1163
|
+
rel = fp.relative_to(js_root).as_posix()
|
|
1164
|
+
if rel in known_relative_paths or rel in seen:
|
|
1165
|
+
continue
|
|
1166
|
+
seen.add(rel)
|
|
1167
|
+
stem = fp.stem.replace(".min", "").replace(".umd", "").replace(".bundle", "")
|
|
1168
|
+
rows.append(
|
|
1169
|
+
{
|
|
1170
|
+
"id": f"local:{stem or fp.name}",
|
|
1171
|
+
"filename": fp.name,
|
|
1172
|
+
"relative_path": rel,
|
|
1173
|
+
"available": True,
|
|
1174
|
+
"size": int(fp.stat().st_size),
|
|
1175
|
+
"sha256": _sha256_file(fp),
|
|
1176
|
+
"source": "existing-local",
|
|
1177
|
+
"error": "",
|
|
1178
|
+
"match_tokens": [fp.name.lower(), stem.lower(), rel.lower()],
|
|
1179
|
+
"urls": [],
|
|
1180
|
+
"catalog": False,
|
|
1181
|
+
}
|
|
1182
|
+
)
|
|
1183
|
+
return rows
|
|
1184
|
+
|
|
1185
|
+
|
|
982
1186
|
def normalize_ui_language(raw: str | None) -> str:
|
|
983
1187
|
key = str(raw or "").strip()
|
|
984
1188
|
if key in UI_LANGUAGE_LABELS:
|
|
@@ -1105,6 +1309,7 @@ def _detect_os_shell_instruction() -> str:
|
|
|
1105
1309
|
"Package manager is 'brew'. "
|
|
1106
1310
|
"Environment variables WORKSPACE_ROOT, SESSION_ROOT, and SKILLS_ROOT are available. "
|
|
1107
1311
|
"Virtual aliases '/workspace/...' and '/skills/...' are supported in shell commands and rewritten to real paths before execution. "
|
|
1312
|
+
"Virtual alias '/js_lib/...' maps to the offline JS libraries root ($JS_LIB_ROOT) and is also supported in file tools. "
|
|
1108
1313
|
"Do NOT assume Linux-specific paths like /proc or /etc/os-release exist. "
|
|
1109
1314
|
"IMPORTANT: The workspace path contains spaces. Always use relative paths "
|
|
1110
1315
|
"(e.g., 'ls uploaded/' not 'ls /full/absolute/path/uploaded/'). "
|
|
@@ -1253,6 +1458,28 @@ def extract_ui_style_setting(raw: object) -> str | None:
|
|
|
1253
1458
|
return None
|
|
1254
1459
|
|
|
1255
1460
|
|
|
1461
|
+
def extract_js_lib_download_setting(raw: object) -> bool | None:
|
|
1462
|
+
"""Read download_js_lib flag from config dict.
|
|
1463
|
+
Keys accepted: download_js_lib / js_lib_download / enable_js_lib_download
|
|
1464
|
+
Sections searched: top-level, then 'startup' / 'offline' / 'web_ui'.
|
|
1465
|
+
Returns True/False, or None if key absent (caller uses default=True).
|
|
1466
|
+
"""
|
|
1467
|
+
if not isinstance(raw, dict):
|
|
1468
|
+
return None
|
|
1469
|
+
keys = ("download_js_lib", "js_lib_download", "enable_js_lib_download", "offline_js_download")
|
|
1470
|
+
for key in keys:
|
|
1471
|
+
if key in raw:
|
|
1472
|
+
return _to_bool_like(raw.get(key), default=True)
|
|
1473
|
+
for section_key in ("startup", "offline", "web_ui", "ui"):
|
|
1474
|
+
section = raw.get(section_key)
|
|
1475
|
+
if not isinstance(section, dict):
|
|
1476
|
+
continue
|
|
1477
|
+
for key in keys:
|
|
1478
|
+
if key in section:
|
|
1479
|
+
return _to_bool_like(section.get(key), default=True)
|
|
1480
|
+
return None
|
|
1481
|
+
|
|
1482
|
+
|
|
1256
1483
|
def default_multimodal_capabilities() -> dict[str, bool]:
|
|
1257
1484
|
return {
|
|
1258
1485
|
"input_image": False,
|
|
@@ -1618,20 +1845,194 @@ def _download_http_bytes(url: str, timeout: float = 25.0) -> tuple[bytes, str]:
|
|
|
1618
1845
|
def offline_js_lib_root(workdir: Path = WORKDIR) -> Path:
|
|
1619
1846
|
return (workdir / "js_lib").resolve()
|
|
1620
1847
|
|
|
1848
|
+
def _offline_js_entry_relative_path(entry: dict[str, object], fallback_name: str) -> str:
|
|
1849
|
+
rel = _normalize_js_lib_asset_ref(str(entry.get("relative_path", "") or ""))
|
|
1850
|
+
if rel:
|
|
1851
|
+
return rel
|
|
1852
|
+
return _safe_js_filename(fallback_name, fallback_name)
|
|
1853
|
+
|
|
1854
|
+
def _archive_member_relative_path(name: str) -> str:
|
|
1855
|
+
raw = _normalize_js_lib_asset_ref(name)
|
|
1856
|
+
if not raw:
|
|
1857
|
+
return ""
|
|
1858
|
+
parts = [part for part in PurePosixPath(raw).parts if part not in {"", "."}]
|
|
1859
|
+
while parts and parts[0].lower() == "package":
|
|
1860
|
+
parts = parts[1:]
|
|
1861
|
+
if not parts:
|
|
1862
|
+
return ""
|
|
1863
|
+
return "/".join(parts)
|
|
1864
|
+
|
|
1865
|
+
def _path_size_bytes(target: Path) -> int:
|
|
1866
|
+
try:
|
|
1867
|
+
if target.is_file():
|
|
1868
|
+
return int(target.stat().st_size)
|
|
1869
|
+
if not target.exists():
|
|
1870
|
+
return 0
|
|
1871
|
+
total = 0
|
|
1872
|
+
for fp in target.rglob("*"):
|
|
1873
|
+
try:
|
|
1874
|
+
if fp.is_file():
|
|
1875
|
+
total += int(fp.stat().st_size)
|
|
1876
|
+
except Exception:
|
|
1877
|
+
continue
|
|
1878
|
+
return total
|
|
1879
|
+
except Exception:
|
|
1880
|
+
return 0
|
|
1881
|
+
|
|
1882
|
+
def _extract_archive_to_dir(raw: bytes, install_root: Path) -> list[str]:
|
|
1883
|
+
install_root.mkdir(parents=True, exist_ok=True)
|
|
1884
|
+
install_root_resolved = install_root.resolve()
|
|
1885
|
+
written: list[str] = []
|
|
1886
|
+
|
|
1887
|
+
def _write_bytes(rel: str, data: bytes):
|
|
1888
|
+
target = (install_root / rel).resolve()
|
|
1889
|
+
if not target.is_relative_to(install_root_resolved):
|
|
1890
|
+
raise ValueError(f"archive member escapes target dir: {rel}")
|
|
1891
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1892
|
+
target.write_bytes(data)
|
|
1893
|
+
written.append(rel)
|
|
1894
|
+
|
|
1895
|
+
bio = io.BytesIO(raw)
|
|
1896
|
+
try:
|
|
1897
|
+
if zipfile.is_zipfile(bio):
|
|
1898
|
+
bio.seek(0)
|
|
1899
|
+
with zipfile.ZipFile(bio, "r") as zf:
|
|
1900
|
+
for info in zf.infolist():
|
|
1901
|
+
if info.is_dir():
|
|
1902
|
+
continue
|
|
1903
|
+
rel = _archive_member_relative_path(info.filename)
|
|
1904
|
+
if not rel:
|
|
1905
|
+
continue
|
|
1906
|
+
_write_bytes(rel, zf.read(info))
|
|
1907
|
+
return written
|
|
1908
|
+
except Exception:
|
|
1909
|
+
pass
|
|
1910
|
+
|
|
1911
|
+
with tarfile.open(fileobj=io.BytesIO(raw), mode="r:*") as tf:
|
|
1912
|
+
for member in tf.getmembers():
|
|
1913
|
+
if not member.isfile():
|
|
1914
|
+
continue
|
|
1915
|
+
rel = _archive_member_relative_path(member.name)
|
|
1916
|
+
if not rel:
|
|
1917
|
+
continue
|
|
1918
|
+
extracted = tf.extractfile(member)
|
|
1919
|
+
if extracted is None:
|
|
1920
|
+
continue
|
|
1921
|
+
_write_bytes(rel, extracted.read())
|
|
1922
|
+
return written
|
|
1923
|
+
|
|
1924
|
+
def _package_required_paths(entry: dict[str, object]) -> list[str]:
|
|
1925
|
+
rows: list[str] = []
|
|
1926
|
+
for item in entry.get("package_required_paths") or []:
|
|
1927
|
+
rel = _normalize_js_lib_asset_ref(str(item or ""))
|
|
1928
|
+
if rel:
|
|
1929
|
+
rows.append(rel)
|
|
1930
|
+
return rows
|
|
1931
|
+
|
|
1932
|
+
def _package_install_ready(install_root: Path, required_paths: list[str]) -> bool:
|
|
1933
|
+
if not install_root.exists():
|
|
1934
|
+
return False
|
|
1935
|
+
if required_paths:
|
|
1936
|
+
return all((install_root / rel).is_file() for rel in required_paths)
|
|
1937
|
+
try:
|
|
1938
|
+
return any(fp.is_file() for fp in install_root.rglob("*"))
|
|
1939
|
+
except Exception:
|
|
1940
|
+
return False
|
|
1941
|
+
|
|
1942
|
+
def _postprocess_offline_js_package(entry: dict[str, object], install_root: Path):
|
|
1943
|
+
action = str(entry.get("package_postprocess", "") or "").strip().lower()
|
|
1944
|
+
if action != "jszip-main-to-dist":
|
|
1945
|
+
return
|
|
1946
|
+
pkg_json = (install_root / "package.json").resolve()
|
|
1947
|
+
dist_fp = (install_root / "dist" / "jszip.min.js").resolve()
|
|
1948
|
+
if not pkg_json.exists():
|
|
1949
|
+
if not dist_fp.exists():
|
|
1950
|
+
return
|
|
1951
|
+
obj: dict[str, object] = {
|
|
1952
|
+
"name": "jszip",
|
|
1953
|
+
"version": "offline-local",
|
|
1954
|
+
"main": "./dist/jszip.min.js",
|
|
1955
|
+
"exports": {".": "./dist/jszip.min.js"},
|
|
1956
|
+
}
|
|
1957
|
+
else:
|
|
1958
|
+
try:
|
|
1959
|
+
raw = pkg_json.read_text(encoding="utf-8")
|
|
1960
|
+
loaded = json.loads(raw)
|
|
1961
|
+
if not isinstance(loaded, dict):
|
|
1962
|
+
return
|
|
1963
|
+
obj = loaded
|
|
1964
|
+
except Exception:
|
|
1965
|
+
return
|
|
1966
|
+
changed = False
|
|
1967
|
+
if obj.get("main") != "./dist/jszip.min.js":
|
|
1968
|
+
obj["main"] = "./dist/jszip.min.js"
|
|
1969
|
+
changed = True
|
|
1970
|
+
exports = obj.get("exports")
|
|
1971
|
+
desired_exports = {".": "./dist/jszip.min.js"}
|
|
1972
|
+
if exports != desired_exports:
|
|
1973
|
+
obj["exports"] = desired_exports
|
|
1974
|
+
changed = True
|
|
1975
|
+
if changed or (not pkg_json.exists()):
|
|
1976
|
+
pkg_json.parent.mkdir(parents=True, exist_ok=True)
|
|
1977
|
+
pkg_json.write_text(json_dumps(obj, indent=2), encoding="utf-8")
|
|
1978
|
+
|
|
1979
|
+
def _ensure_offline_js_package(root: Path, entry: dict[str, object], force: bool = False) -> tuple[bool, str, str, str]:
|
|
1980
|
+
package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
|
|
1981
|
+
if not package_urls:
|
|
1982
|
+
return True, "", "", ""
|
|
1983
|
+
install_dir = _normalize_js_lib_asset_ref(str(entry.get("package_install_dir", "") or ""))
|
|
1984
|
+
if not install_dir:
|
|
1985
|
+
install_dir = _normalize_js_lib_asset_ref(str(entry.get("id", "") or "package"))
|
|
1986
|
+
if not install_dir:
|
|
1987
|
+
return False, "missing", "package install dir is empty", ""
|
|
1988
|
+
install_root = (root / install_dir).resolve()
|
|
1989
|
+
if not install_root.is_relative_to(root):
|
|
1990
|
+
return False, "missing", f"package install dir escapes js_lib: {install_dir}", install_dir
|
|
1991
|
+
required_paths = _package_required_paths(entry)
|
|
1992
|
+
if not force and install_root.exists():
|
|
1993
|
+
_postprocess_offline_js_package(entry, install_root)
|
|
1994
|
+
if _package_install_ready(install_root, required_paths):
|
|
1995
|
+
return True, "existing-package", "", install_dir
|
|
1996
|
+
try:
|
|
1997
|
+
if any(fp.is_file() for fp in install_root.rglob("*")):
|
|
1998
|
+
return True, "existing-package", "", install_dir
|
|
1999
|
+
except Exception:
|
|
2000
|
+
pass
|
|
2001
|
+
error = ""
|
|
2002
|
+
source = "missing"
|
|
2003
|
+
for url in package_urls:
|
|
2004
|
+
try:
|
|
2005
|
+
data, _ = _download_http_bytes(url, timeout=90.0)
|
|
2006
|
+
if len(data) < 200:
|
|
2007
|
+
error = f"archive too small from {url}"
|
|
2008
|
+
continue
|
|
2009
|
+
_extract_archive_to_dir(data, install_root)
|
|
2010
|
+
_postprocess_offline_js_package(entry, install_root)
|
|
2011
|
+
if _package_install_ready(install_root, required_paths):
|
|
2012
|
+
return True, url, "", install_dir
|
|
2013
|
+
error = f"package install incomplete from {url}"
|
|
2014
|
+
source = url
|
|
2015
|
+
except Exception as exc:
|
|
2016
|
+
error = trim(str(exc), 220)
|
|
2017
|
+
source = url
|
|
2018
|
+
return False, source, error, install_dir
|
|
2019
|
+
|
|
1621
2020
|
def _render_offline_js_catalog_md() -> str:
|
|
1622
2021
|
rows = [
|
|
1623
2022
|
"# Offline JS Library Catalog",
|
|
1624
2023
|
"",
|
|
1625
2024
|
"Pre-fetched common JS libraries for offline HTML deliverables.",
|
|
2025
|
+
"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
2026
|
"",
|
|
1627
|
-
"| id |
|
|
1628
|
-
"
|
|
2027
|
+
"| id | relative path | file urls | package urls |",
|
|
2028
|
+
"|---|---|---|---|",
|
|
1629
2029
|
]
|
|
1630
2030
|
for row in OFFLINE_JS_LIB_CATALOG:
|
|
1631
2031
|
lib_id = str(row.get("id", "") or "").strip()
|
|
1632
|
-
filename = str(row.get("filename", "") or "")
|
|
2032
|
+
filename = _offline_js_entry_relative_path(row, str(row.get("filename", "") or lib_id or "lib.js"))
|
|
1633
2033
|
urls = [str(x).strip() for x in (row.get("urls") or []) if str(x).strip()]
|
|
1634
|
-
|
|
2034
|
+
package_urls = [str(x).strip() for x in (row.get("package_urls") or []) if str(x).strip()]
|
|
2035
|
+
rows.append(f"| `{lib_id}` | `{filename}` | {'<br>'.join(urls)} | {'<br>'.join(package_urls)} |")
|
|
1635
2036
|
return "\n".join(rows) + "\n"
|
|
1636
2037
|
|
|
1637
2038
|
def load_offline_js_lib_index(js_root: Path) -> dict:
|
|
@@ -1645,26 +2046,78 @@ def load_offline_js_lib_index(js_root: Path) -> dict:
|
|
|
1645
2046
|
except Exception:
|
|
1646
2047
|
return {}
|
|
1647
2048
|
|
|
1648
|
-
def ensure_offline_js_libs(
|
|
2049
|
+
def ensure_offline_js_libs(
|
|
2050
|
+
workdir: Path = WORKDIR,
|
|
2051
|
+
force: bool = False,
|
|
2052
|
+
verbose: bool = False,
|
|
2053
|
+
no_connection_deadline: float = 60.0,
|
|
2054
|
+
) -> dict:
|
|
1649
2055
|
root = offline_js_lib_root(workdir)
|
|
1650
2056
|
root.mkdir(parents=True, exist_ok=True)
|
|
1651
2057
|
fetched = 0
|
|
1652
2058
|
available = 0
|
|
1653
2059
|
missing = 0
|
|
2060
|
+
_dl_start = time.time()
|
|
2061
|
+
_deadline_hit = False
|
|
2062
|
+
total_catalog = len(OFFLINE_JS_LIB_CATALOG)
|
|
2063
|
+
if verbose:
|
|
2064
|
+
print(f"[js_lib] Starting JS library download ({total_catalog} files)...", flush=True)
|
|
1654
2065
|
rows: list[dict] = []
|
|
2066
|
+
known_relative_paths: set[str] = set()
|
|
1655
2067
|
for entry in OFFLINE_JS_LIB_CATALOG:
|
|
1656
2068
|
lib_id = str(entry.get("id", "") or "").strip() or "lib"
|
|
1657
2069
|
filename = _safe_js_filename(str(entry.get("filename", "") or f"{lib_id}.js"), f"{lib_id}.js")
|
|
2070
|
+
relative_path = _offline_js_entry_relative_path(entry, filename)
|
|
1658
2071
|
urls = [str(x).strip() for x in (entry.get("urls") or []) if str(x).strip()]
|
|
2072
|
+
package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
|
|
1659
2073
|
match_tokens = [str(x).strip().lower() for x in (entry.get("match_tokens") or []) if str(x).strip()]
|
|
1660
|
-
target = root /
|
|
2074
|
+
target = (root / relative_path).resolve()
|
|
2075
|
+
if not target.is_relative_to(root):
|
|
2076
|
+
rows.append(
|
|
2077
|
+
{
|
|
2078
|
+
"id": lib_id,
|
|
2079
|
+
"filename": filename,
|
|
2080
|
+
"available": False,
|
|
2081
|
+
"size": 0,
|
|
2082
|
+
"sha256": "",
|
|
2083
|
+
"source": "missing",
|
|
2084
|
+
"error": f"relative path escapes js_lib: {relative_path}",
|
|
2085
|
+
"match_tokens": match_tokens,
|
|
2086
|
+
"urls": urls,
|
|
2087
|
+
"package_urls": package_urls,
|
|
2088
|
+
"relative_path": relative_path,
|
|
2089
|
+
"package_install_dir": "",
|
|
2090
|
+
"package_required_paths": [],
|
|
2091
|
+
"package_available": not package_urls,
|
|
2092
|
+
"package_size": 0,
|
|
2093
|
+
"catalog": True,
|
|
2094
|
+
}
|
|
2095
|
+
)
|
|
2096
|
+
missing += 1
|
|
2097
|
+
continue
|
|
2098
|
+
known_relative_paths.add(relative_path)
|
|
1661
2099
|
source = "existing"
|
|
1662
2100
|
error = ""
|
|
1663
|
-
|
|
1664
|
-
|
|
2101
|
+
effective_target = target
|
|
2102
|
+
file_ok = bool(target.exists() and target.is_file() and target.stat().st_size > 40 and (not force))
|
|
2103
|
+
if (not file_ok) and (not force):
|
|
2104
|
+
resolved_existing = _resolve_js_lib_asset_path(root, relative_path)
|
|
2105
|
+
if resolved_existing and resolved_existing.exists() and resolved_existing.is_file() and resolved_existing.stat().st_size > 40:
|
|
2106
|
+
effective_target = resolved_existing
|
|
2107
|
+
file_ok = True
|
|
2108
|
+
source = "existing-local"
|
|
2109
|
+
if not file_ok and urls:
|
|
2110
|
+
if fetched == 0 and (time.time() - _dl_start) > no_connection_deadline:
|
|
2111
|
+
_deadline_hit = True
|
|
2112
|
+
if verbose:
|
|
2113
|
+
print(f"[js_lib] No connection after {no_connection_deadline:.0f}s — skipping remaining downloads.", flush=True)
|
|
2114
|
+
break
|
|
2115
|
+
if verbose:
|
|
2116
|
+
print(f"[js_lib] Downloading {filename}...", flush=True)
|
|
1665
2117
|
for url in urls:
|
|
1666
2118
|
try:
|
|
1667
|
-
|
|
2119
|
+
_timeout = 12.0 if fetched == 0 else 35.0
|
|
2120
|
+
data, _ = _download_http_bytes(url, timeout=_timeout)
|
|
1668
2121
|
if len(data) < 40:
|
|
1669
2122
|
error = f"download too small from {url}"
|
|
1670
2123
|
continue
|
|
@@ -1672,39 +2125,67 @@ def ensure_offline_js_libs(workdir: Path = WORKDIR, force: bool = False) -> dict
|
|
|
1672
2125
|
if "<html" in probe and "<script" not in probe and "tailwind" not in probe:
|
|
1673
2126
|
error = f"unexpected html payload from {url}"
|
|
1674
2127
|
continue
|
|
2128
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1675
2129
|
target.write_bytes(data)
|
|
1676
|
-
|
|
2130
|
+
effective_target = target
|
|
2131
|
+
file_ok = True
|
|
1677
2132
|
source = url
|
|
1678
2133
|
fetched += 1
|
|
1679
2134
|
break
|
|
1680
2135
|
except Exception as exc:
|
|
1681
2136
|
error = trim(str(exc), 220)
|
|
1682
|
-
if not
|
|
2137
|
+
if not file_ok:
|
|
1683
2138
|
source = "missing"
|
|
2139
|
+
package_ok, package_source, package_error, package_install_dir = _ensure_offline_js_package(root, entry, force=force)
|
|
2140
|
+
if package_urls and package_source and package_source not in {"existing-package", "missing"}:
|
|
2141
|
+
fetched += 1
|
|
2142
|
+
if package_error:
|
|
2143
|
+
error = package_error if not error else f"{error}; {package_error}"
|
|
2144
|
+
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
2145
|
if ok:
|
|
1685
2146
|
available += 1
|
|
1686
2147
|
else:
|
|
1687
2148
|
missing += 1
|
|
2149
|
+
if package_urls and package_source == "missing" and not source.strip():
|
|
2150
|
+
source = "missing"
|
|
2151
|
+
package_root = (root / package_install_dir).resolve() if package_install_dir else None
|
|
1688
2152
|
rows.append(
|
|
1689
2153
|
{
|
|
1690
2154
|
"id": lib_id,
|
|
1691
2155
|
"filename": filename,
|
|
1692
2156
|
"available": ok,
|
|
1693
|
-
"size": int(
|
|
1694
|
-
"sha256": _sha256_file(
|
|
1695
|
-
"source": source,
|
|
2157
|
+
"size": int(effective_target.stat().st_size) if effective_target.exists() else 0,
|
|
2158
|
+
"sha256": _sha256_file(effective_target) if effective_target.exists() else "",
|
|
2159
|
+
"source": source if source != "existing" or file_ok else package_source or source,
|
|
1696
2160
|
"error": error,
|
|
1697
2161
|
"match_tokens": match_tokens,
|
|
1698
2162
|
"urls": urls,
|
|
2163
|
+
"package_urls": package_urls,
|
|
2164
|
+
"relative_path": relative_path,
|
|
2165
|
+
"resolved_path": effective_target.relative_to(root).as_posix() if effective_target.exists() else "",
|
|
2166
|
+
"package_install_dir": package_install_dir,
|
|
2167
|
+
"package_required_paths": _package_required_paths(entry),
|
|
2168
|
+
"package_available": package_ok,
|
|
2169
|
+
"package_size": _path_size_bytes(package_root) if package_root else 0,
|
|
2170
|
+
"package_source": package_source,
|
|
2171
|
+
"catalog": True,
|
|
1699
2172
|
}
|
|
1700
2173
|
)
|
|
2174
|
+
extra_rows = _discover_extra_js_lib_files(root, known_relative_paths)
|
|
2175
|
+
rows.extend(extra_rows)
|
|
2176
|
+
if verbose:
|
|
2177
|
+
_status = "skipped (no connection)" if _deadline_hit else f"{fetched} downloaded"
|
|
2178
|
+
print(f"[js_lib] Done: {available}/{len(rows)} available, {_status}.", flush=True)
|
|
1701
2179
|
payload = {
|
|
1702
2180
|
"generated_at": int(now_ts()),
|
|
1703
2181
|
"js_lib_root": str(root),
|
|
1704
|
-
"total": len(
|
|
2182
|
+
"total": len(rows),
|
|
1705
2183
|
"available": available,
|
|
1706
2184
|
"missing": missing,
|
|
1707
2185
|
"fetched": fetched,
|
|
2186
|
+
"catalog_total": len(OFFLINE_JS_LIB_CATALOG),
|
|
2187
|
+
"catalog_missing": missing,
|
|
2188
|
+
"extra_local": len(extra_rows),
|
|
1708
2189
|
"libs": rows,
|
|
1709
2190
|
}
|
|
1710
2191
|
(root / OFFLINE_JS_LIB_INDEX_FILE).write_text(json_dumps(payload, indent=2), encoding="utf-8")
|
|
@@ -9043,7 +9524,12 @@ TOOLS = [
|
|
|
9043
9524
|
),
|
|
9044
9525
|
tool_def(
|
|
9045
9526
|
"query_knowledge_library",
|
|
9046
|
-
|
|
9527
|
+
(
|
|
9528
|
+
"Query the RAG knowledge library for grounded document references and background knowledge. "
|
|
9529
|
+
"Call this BEFORE answering questions that require domain expertise, factual grounding, "
|
|
9530
|
+
"or synthesis from imported documents. "
|
|
9531
|
+
"Pass an empty query to check library status only."
|
|
9532
|
+
),
|
|
9047
9533
|
{
|
|
9048
9534
|
"query": {"type": "string"},
|
|
9049
9535
|
"top_k": {"type": "integer"},
|
|
@@ -10780,6 +11266,32 @@ class SessionState:
|
|
|
10780
11266
|
self.runtime_code_reference_meta = {}
|
|
10781
11267
|
return removed_hints
|
|
10782
11268
|
|
|
11269
|
+
def _reset_blackboard_plan_state_locked(self) -> None:
|
|
11270
|
+
"""Clear plan/todo/skills state from a completed run so the next run starts fresh.
|
|
11271
|
+
|
|
11272
|
+
Called from submit_user_message when a new user request arrives after a
|
|
11273
|
+
previous run finished (running=False, not awaiting plan choice).
|
|
11274
|
+
Prevents the manager from seeing status=COMPLETED + all todos done and
|
|
11275
|
+
immediately routing to 'finish' again on the very first round.
|
|
11276
|
+
"""
|
|
11277
|
+
bb = self._ensure_blackboard()
|
|
11278
|
+
bb["project_todos"] = []
|
|
11279
|
+
bb["plan_steps"] = []
|
|
11280
|
+
bb["plan_step_cursor"] = 0
|
|
11281
|
+
bb["plan_step_total"] = 0
|
|
11282
|
+
bb["status"] = ""
|
|
11283
|
+
bb["approval"] = ""
|
|
11284
|
+
bb["plan_findings"] = ""
|
|
11285
|
+
bb["plan_proposal"] = ""
|
|
11286
|
+
bb["plan_risks"] = ""
|
|
11287
|
+
bb["loaded_skills"] = {}
|
|
11288
|
+
bb["loaded_skills_goal_sig"] = ""
|
|
11289
|
+
self.blackboard = bb
|
|
11290
|
+
try:
|
|
11291
|
+
self.todo.items = []
|
|
11292
|
+
except Exception:
|
|
11293
|
+
pass
|
|
11294
|
+
|
|
10783
11295
|
def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
|
|
10784
11296
|
payload = dict(data or {})
|
|
10785
11297
|
if self._sanitize_agent_bubble_role(payload.get("agent_role", "")):
|
|
@@ -11133,7 +11645,10 @@ class SessionState:
|
|
|
11133
11645
|
skill_desc = str(skill_row.get("description", "-")).strip()
|
|
11134
11646
|
inject_msg = (
|
|
11135
11647
|
f"<loaded-skill name=\"{skill_key}\">\n"
|
|
11136
|
-
f"A skill has been loaded.
|
|
11648
|
+
f"A skill has been loaded. IMPORTANT: This skill's workflow, tools, and commands "
|
|
11649
|
+
f"OVERRIDE the plan's implementation approach for any step where it applies. "
|
|
11650
|
+
f"Read the full instructions below and follow them exactly — do NOT substitute a "
|
|
11651
|
+
f"different tool, library, or language unless the skill explicitly allows it.\n"
|
|
11137
11652
|
f"{trim(skill_text, 12000)}\n"
|
|
11138
11653
|
f"</loaded-skill>"
|
|
11139
11654
|
)
|
|
@@ -11594,14 +12109,28 @@ class SessionState:
|
|
|
11594
12109
|
)
|
|
11595
12110
|
return (
|
|
11596
12111
|
f"ACTIVE SKILLS: {names}. "
|
|
11597
|
-
"Follow loaded skill instructions
|
|
11598
|
-
f"
|
|
12112
|
+
"Follow the loaded skill instructions for the current step. "
|
|
12113
|
+
f"When moving to a different step that needs a DIFFERENT skill, call load_skill to switch "
|
|
12114
|
+
f"(or unload the current one first if it's no longer needed). "
|
|
12115
|
+
f"{skill_count} skills available total. "
|
|
11599
12116
|
)
|
|
11600
12117
|
return (
|
|
11601
12118
|
f"SKILL SYSTEM: {skill_count} skills available. "
|
|
11602
|
-
"
|
|
11603
|
-
"
|
|
11604
|
-
|
|
12119
|
+
"Skills are loaded ON-DEMAND — decide when you need one based on the CURRENT step, not upfront. "
|
|
12120
|
+
"For specialized output (reports, slides/PPT, deep research, code review, PDF analysis): "
|
|
12121
|
+
"call list_skills to discover options, then load_skill to activate the right one. "
|
|
12122
|
+
"Load a skill AT THE MOMENT you begin the step that requires it. "
|
|
12123
|
+
"Unload it (via unload_skill) when moving to a different step that needs a different skill. "
|
|
12124
|
+
"For simple tasks, direct questions, and multimodal analysis, do NOT load skills. "
|
|
12125
|
+
)
|
|
12126
|
+
|
|
12127
|
+
def _skills_awareness_block(self, for_role: str = "developer") -> str:
|
|
12128
|
+
"""Canonical skills-awareness block shared by single, sync, and plan-mode.
|
|
12129
|
+
Returns: loaded-skills hint + newline + 'Skills:\\n<catalog>'
|
|
12130
|
+
Keeps all three modes in sync — change here propagates everywhere.
|
|
12131
|
+
"""
|
|
12132
|
+
hint = self._loaded_skills_prompt_hint(for_role=for_role)
|
|
12133
|
+
return f"{hint}\nSkills:\n{self.skills.descriptions()}\n"
|
|
11605
12134
|
|
|
11606
12135
|
def _refresh_runtime_code_reference(self, text: str):
|
|
11607
12136
|
cb = getattr(self, "reference_prepare_callback", None)
|
|
@@ -11663,10 +12192,40 @@ class SessionState:
|
|
|
11663
12192
|
header += " [" + ", ".join(tags) + "]"
|
|
11664
12193
|
return (
|
|
11665
12194
|
f"{header}:\n"
|
|
11666
|
-
"
|
|
11667
|
-
"
|
|
11668
|
-
"
|
|
11669
|
-
"
|
|
12195
|
+
"Global TF-Graph_IDF RAG library for imported documents, PDFs, and research material. "
|
|
12196
|
+
"IMPORTANT: When the task involves a topic you may have documents for — research, analysis, "
|
|
12197
|
+
"fact-checking, synthesis — FIRST call query_knowledge_library(query='<topic>', top_k=8) "
|
|
12198
|
+
"to retrieve grounded references BEFORE generating your answer. "
|
|
12199
|
+
"Use route='hybrid' for best recall on broad topics; route='fast' for keyword lookups. "
|
|
12200
|
+
"Do not infer readiness from session/files or uploads — query the library directly."
|
|
12201
|
+
)
|
|
12202
|
+
|
|
12203
|
+
def _multimodal_capability_block(self) -> str:
|
|
12204
|
+
"""Return a brief system-prompt note about native multimodal capabilities.
|
|
12205
|
+
|
|
12206
|
+
Injected into system prompt so the model knows it can directly analyze
|
|
12207
|
+
images/audio/video rather than falling back to text-only workarounds.
|
|
12208
|
+
Returns empty string when the active model has no multimodal input caps.
|
|
12209
|
+
"""
|
|
12210
|
+
try:
|
|
12211
|
+
caps = self._capabilities_from_profile()
|
|
12212
|
+
except Exception:
|
|
12213
|
+
return ""
|
|
12214
|
+
types = []
|
|
12215
|
+
if caps.get("input_image"):
|
|
12216
|
+
types.append("images")
|
|
12217
|
+
if caps.get("input_audio"):
|
|
12218
|
+
types.append("audio")
|
|
12219
|
+
if caps.get("input_video"):
|
|
12220
|
+
types.append("video")
|
|
12221
|
+
if not types:
|
|
12222
|
+
return ""
|
|
12223
|
+
joined = "/".join(types)
|
|
12224
|
+
return (
|
|
12225
|
+
f"MULTIMODAL: This model supports native {joined} analysis. "
|
|
12226
|
+
f"When {joined} are attached or loaded via read_file, analyze them DIRECTLY "
|
|
12227
|
+
"using your built-in perception capabilities — do not describe them as "
|
|
12228
|
+
"inaccessible or attempt text-only workarounds. "
|
|
11670
12229
|
)
|
|
11671
12230
|
|
|
11672
12231
|
def _code_library_prompt_block(self) -> str:
|
|
@@ -11747,8 +12306,14 @@ class SessionState:
|
|
|
11747
12306
|
plan_ctx = self._plan_steps_context_for_manager()
|
|
11748
12307
|
if plan_ctx:
|
|
11749
12308
|
plan_steps_block = f"{plan_ctx}\n"
|
|
12309
|
+
mm_block = self._multimodal_capability_block()
|
|
12310
|
+
mm_hint = f"{mm_block}\n" if mm_block else ""
|
|
11750
12311
|
return (
|
|
11751
|
-
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
12312
|
+
f"You are a coding agent. Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
|
|
12313
|
+
f"Offline JS libraries root: $JS_LIB_ROOT. "
|
|
12314
|
+
f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
|
|
12315
|
+
f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS require) or pptxgen.bundle.js (browser). "
|
|
12316
|
+
f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
|
|
11752
12317
|
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
11753
12318
|
f"budget={'unlimited' if budget <= 0 else budget}. "
|
|
11754
12319
|
f"Context limit ~{self.context_token_upper_bound} tokens. "
|
|
@@ -11756,6 +12321,7 @@ class SessionState:
|
|
|
11756
12321
|
"Use tools to inspect, edit, and execute. "
|
|
11757
12322
|
"Call finish_current_task when done. "
|
|
11758
12323
|
f"{skill_hint}"
|
|
12324
|
+
f"{mm_hint}"
|
|
11759
12325
|
f"{plan_steps_block}"
|
|
11760
12326
|
f"{html_block}"
|
|
11761
12327
|
f"{research_block}"
|
|
@@ -13540,6 +14106,11 @@ class SessionState:
|
|
|
13540
14106
|
if low.startswith("/workspace/"):
|
|
13541
14107
|
rel = raw[len("/workspace/") :].lstrip("/")
|
|
13542
14108
|
return rel or "."
|
|
14109
|
+
if low in {"/js_lib", "/js_lib/"}:
|
|
14110
|
+
return ".__js_lib__"
|
|
14111
|
+
if low.startswith("/js_lib/"):
|
|
14112
|
+
rel = raw[len("/js_lib/") :].lstrip("/")
|
|
14113
|
+
return f".__js_lib__/{rel}" if rel else ".__js_lib__"
|
|
13543
14114
|
if low in {SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}:
|
|
13544
14115
|
return ".__skills__"
|
|
13545
14116
|
if low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/"):
|
|
@@ -13563,9 +14134,10 @@ class SessionState:
|
|
|
13563
14134
|
return ""
|
|
13564
14135
|
low = txt.lower()
|
|
13565
14136
|
if (
|
|
13566
|
-
low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}
|
|
14137
|
+
low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/", "/js_lib", "/js_lib/"}
|
|
13567
14138
|
or low.startswith("/workspace/")
|
|
13568
14139
|
or low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/")
|
|
14140
|
+
or low.startswith("/js_lib/")
|
|
13569
14141
|
):
|
|
13570
14142
|
return ""
|
|
13571
14143
|
return (
|
|
@@ -13578,6 +14150,9 @@ class SessionState:
|
|
|
13578
14150
|
if normalized == ".__skills__" or normalized.startswith(".__skills__/"):
|
|
13579
14151
|
rel = normalized[len(".__skills__") :].lstrip("/")
|
|
13580
14152
|
return safe_path(rel or ".", self.skills.skills_root)
|
|
14153
|
+
if normalized == ".__js_lib__" or normalized.startswith(".__js_lib__/"):
|
|
14154
|
+
rel = normalized[len(".__js_lib__") :].lstrip("/")
|
|
14155
|
+
return safe_path(rel or ".", self.js_lib_root)
|
|
13581
14156
|
return safe_path(normalized, self.files_root)
|
|
13582
14157
|
|
|
13583
14158
|
def _session_rel(self, path: Path) -> str:
|
|
@@ -13585,6 +14160,13 @@ class SessionState:
|
|
|
13585
14160
|
root = self.files_root.resolve()
|
|
13586
14161
|
if target.is_relative_to(root):
|
|
13587
14162
|
return target.relative_to(root).as_posix()
|
|
14163
|
+
try:
|
|
14164
|
+
js_lib_root = self.js_lib_root.resolve()
|
|
14165
|
+
if target.is_relative_to(js_lib_root):
|
|
14166
|
+
rel = target.relative_to(js_lib_root).as_posix()
|
|
14167
|
+
return f"/js_lib/{rel}".replace("//", "/")
|
|
14168
|
+
except Exception:
|
|
14169
|
+
pass
|
|
13588
14170
|
try:
|
|
13589
14171
|
skills_root = self.skills.skills_root.resolve()
|
|
13590
14172
|
if target.is_relative_to(skills_root):
|
|
@@ -13962,8 +14544,20 @@ class SessionState:
|
|
|
13962
14544
|
}
|
|
13963
14545
|
|
|
13964
14546
|
def _safe_upload_name(self, filename: str) -> str:
|
|
14547
|
+
# Use Path().name to strip any directory component first.
|
|
13965
14548
|
raw = Path(str(filename or "upload.bin")).name
|
|
13966
|
-
|
|
14549
|
+
# Only remove characters that are genuinely illegal or dangerous on
|
|
14550
|
+
# filesystems (path separators, null byte, control chars, Windows
|
|
14551
|
+
# reserved chars). Unicode letters/CJK/etc. are left untouched so
|
|
14552
|
+
# filenames like "我的数据.xlsx" remain readable.
|
|
14553
|
+
safe = re.sub(r'[/\\\x00-\x1f\x7f:*?"<>|]', "_", raw)
|
|
14554
|
+
safe = safe.strip(". ") # avoid hidden files (leading dot) and trailing issues
|
|
14555
|
+
# Enforce a byte-length ceiling safe across all filesystems (255 bytes max).
|
|
14556
|
+
# Trim the stem if needed while preserving the extension.
|
|
14557
|
+
if len(safe.encode("utf-8", errors="replace")) > 240:
|
|
14558
|
+
ext = Path(safe).suffix # e.g. ".xlsx"
|
|
14559
|
+
stem = safe[: max(1, 200 - len(ext))]
|
|
14560
|
+
safe = stem + ext
|
|
13967
14561
|
return safe or f"upload_{int(now_ts())}.bin"
|
|
13968
14562
|
|
|
13969
14563
|
def _decode_text_bytes(self, data: bytes) -> str:
|
|
@@ -15964,15 +16558,22 @@ class SessionState:
|
|
|
15964
16558
|
skills_root = str(self.skills.skills_root.resolve())
|
|
15965
16559
|
except Exception:
|
|
15966
16560
|
skills_root = str(self.skills.skills_root)
|
|
16561
|
+
try:
|
|
16562
|
+
js_lib_root = str(self.js_lib_root.resolve())
|
|
16563
|
+
except Exception:
|
|
16564
|
+
js_lib_root = str(self.js_lib_root)
|
|
15967
16565
|
mappings = [
|
|
15968
16566
|
("/workspace", workspace_root),
|
|
15969
16567
|
(SKILLS_VIRTUAL_PREFIX, skills_root),
|
|
16568
|
+
("/js_lib", js_lib_root),
|
|
15970
16569
|
]
|
|
15971
16570
|
# Auto-quote raw absolute paths that contain spaces (prevents word-splitting)
|
|
15972
16571
|
if " " in workspace_root and workspace_root not in ("/workspace",):
|
|
15973
16572
|
mappings.append((workspace_root, workspace_root))
|
|
15974
16573
|
if " " in skills_root and skills_root != SKILLS_VIRTUAL_PREFIX:
|
|
15975
16574
|
mappings.append((skills_root, skills_root))
|
|
16575
|
+
if " " in js_lib_root and js_lib_root != "/js_lib":
|
|
16576
|
+
mappings.append((js_lib_root, js_lib_root))
|
|
15976
16577
|
out: list[str] = []
|
|
15977
16578
|
mode = "plain"
|
|
15978
16579
|
i = 0
|
|
@@ -16313,6 +16914,7 @@ class SessionState:
|
|
|
16313
16914
|
proc_env["SESSION_FILES_ROOT"] = str(self.files_root)
|
|
16314
16915
|
proc_env["SKILLS_ROOT"] = str(self.skills.skills_root)
|
|
16315
16916
|
proc_env["CLOUDS_CODER_ROOT"] = str(self.root)
|
|
16917
|
+
proc_env["JS_LIB_ROOT"] = str(self.js_lib_root)
|
|
16316
16918
|
popen_kwargs = {
|
|
16317
16919
|
"shell": True,
|
|
16318
16920
|
"cwd": cwd,
|
|
@@ -16430,10 +17032,31 @@ class SessionState:
|
|
|
16430
17032
|
def _run_bash(self, command: str) -> str:
|
|
16431
17033
|
return self._run_shell_meta(command, self.files_root, 120)["output"]
|
|
16432
17034
|
|
|
17035
|
+
def _fuzzy_resolve_path(self, fp: Path) -> Path:
|
|
17036
|
+
"""If fp doesn't exist, try stripping spaces from the filename to find a close match.
|
|
17037
|
+
Handles the common model error of hallucinating spaces in Chinese/mixed filenames.
|
|
17038
|
+
Returns the resolved Path if found, otherwise the original fp unchanged."""
|
|
17039
|
+
if fp.exists():
|
|
17040
|
+
return fp
|
|
17041
|
+
stripped = fp.name.replace(" ", "")
|
|
17042
|
+
if stripped != fp.name:
|
|
17043
|
+
candidate = fp.parent / stripped
|
|
17044
|
+
if candidate.exists():
|
|
17045
|
+
return candidate
|
|
17046
|
+
try:
|
|
17047
|
+
query = fp.name.replace(" ", "").lower()
|
|
17048
|
+
for sibling in fp.parent.iterdir():
|
|
17049
|
+
if sibling.name.replace(" ", "").lower() == query:
|
|
17050
|
+
return sibling
|
|
17051
|
+
except Exception:
|
|
17052
|
+
pass
|
|
17053
|
+
return fp
|
|
17054
|
+
|
|
16433
17055
|
def _run_read(self, path: str, limit: int | None = None, offset: int | None = None) -> str:
|
|
16434
17056
|
try:
|
|
16435
17057
|
rel = self._normalize_tool_path_text(path)
|
|
16436
|
-
fp = self._session_path(rel)
|
|
17058
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17059
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16437
17060
|
# Multimodal: detect image/audio/video files and handle natively
|
|
16438
17061
|
ext = fp.suffix.lower() if fp.suffix else ""
|
|
16439
17062
|
if ext in IMAGE_EXTS:
|
|
@@ -16680,7 +17303,8 @@ class SessionState:
|
|
|
16680
17303
|
def _run_write(self, path: str, content: str) -> str:
|
|
16681
17304
|
try:
|
|
16682
17305
|
rel = self._normalize_tool_path_text(path)
|
|
16683
|
-
fp = self._session_path(rel)
|
|
17306
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17307
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16684
17308
|
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
16685
17309
|
fp.write_text(content, encoding="utf-8")
|
|
16686
17310
|
return f"Wrote {len(content)} bytes to {rel}"
|
|
@@ -16690,7 +17314,8 @@ class SessionState:
|
|
|
16690
17314
|
def _run_edit(self, path: str, old_text: str, new_text: str) -> str:
|
|
16691
17315
|
try:
|
|
16692
17316
|
rel = self._normalize_tool_path_text(path)
|
|
16693
|
-
fp = self._session_path(rel)
|
|
17317
|
+
fp = self._fuzzy_resolve_path(self._session_path(rel))
|
|
17318
|
+
rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
|
|
16694
17319
|
content = fp.read_text(encoding="utf-8")
|
|
16695
17320
|
if old_text not in content:
|
|
16696
17321
|
diag = self._edit_mismatch_diagnostic(content, old_text)
|
|
@@ -19440,35 +20065,36 @@ class SessionState:
|
|
|
19440
20065
|
if not current:
|
|
19441
20066
|
return False
|
|
19442
20067
|
text = (str(instruction or "") + " " + str(reason or "")).lower()
|
|
19443
|
-
# Patterns that indicate step completion
|
|
20068
|
+
# Patterns that indicate step completion — only BACKWARD-looking signals
|
|
20069
|
+
# (agent/manager explicitly says a step is done, NOT forward-looking dispatch instructions)
|
|
19444
20070
|
step_done_patterns = (
|
|
19445
20071
|
"审查通过", "通过审查", "已通过", "已完成", "完成了",
|
|
19446
|
-
"进入 step", "进入step", "enter step", "move to step",
|
|
19447
20072
|
"step completed", "step done", "step passed",
|
|
19448
|
-
"现在进入", "开始 step", "开始step",
|
|
19449
20073
|
"阶段完成", "阶段通过", "phase complete",
|
|
19450
|
-
|
|
19451
|
-
"
|
|
19452
|
-
"进入下一步", "next step", "proceed to step",
|
|
19453
|
-
"步骤 1.1 已完成", "步骤 1.2 已完成",
|
|
20074
|
+
"步骤 1.1 已完成", "步骤 1.2 已完成", "步骤 1.3 已完成",
|
|
20075
|
+
"步骤 1.4 已完成", "步骤 1.5 已完成",
|
|
19454
20076
|
)
|
|
19455
|
-
#
|
|
20077
|
+
# NOTE: intentionally excluded forward-looking dispatch patterns:
|
|
20078
|
+
# "现在执行步骤", "执行步骤 1.2", "执行步骤 1.3", "进入下一步", "next step",
|
|
20079
|
+
# "proceed to step", "进入 step", "开始 step" — these are manager dispatch
|
|
20080
|
+
# instructions, NOT evidence that the current step was completed.
|
|
19456
20081
|
import re
|
|
19457
20082
|
current_idx = int(current.get("plan_step_index", 0) or 0)
|
|
19458
|
-
#
|
|
20083
|
+
# Only advance when an agent explicitly says "进入 Step N" where N > current+1
|
|
20084
|
+
# (skipping ahead), NOT when manager dispatches the very next step.
|
|
19459
20085
|
next_step_pattern = re.search(
|
|
19460
20086
|
r'(?:进入|enter|move\s+to|start|proceed\s+to)\s*(?:step\s*)?(\d+)',
|
|
19461
20087
|
text, re.IGNORECASE
|
|
19462
20088
|
)
|
|
19463
20089
|
if next_step_pattern:
|
|
19464
20090
|
mentioned_step = int(next_step_pattern.group(1))
|
|
19465
|
-
if mentioned_step > current_idx +
|
|
20091
|
+
if mentioned_step > current_idx + 2: # must skip at least 2 ahead to be meaningful
|
|
19466
20092
|
return True
|
|
19467
|
-
#
|
|
19468
|
-
step_ref = re.search(r'
|
|
20093
|
+
# "完成步骤 1.1,开始 1.2" — only if explicitly marking current step done
|
|
20094
|
+
step_ref = re.search(r'(?:完成|finished|done)\s*步骤\s*1\.(\d+)', text)
|
|
19469
20095
|
if step_ref:
|
|
19470
20096
|
ref_sub = int(step_ref.group(1))
|
|
19471
|
-
if ref_sub
|
|
20097
|
+
if ref_sub == current_idx + 1: # explicitly says current step (1-based) is done
|
|
19472
20098
|
return True
|
|
19473
20099
|
return any(pat in text for pat in step_done_patterns)
|
|
19474
20100
|
|
|
@@ -19524,7 +20150,7 @@ class SessionState:
|
|
|
19524
20150
|
self._sync_todos_from_blackboard(reason=f"plan-step-advanced:{cursor + 1}", board=bb)
|
|
19525
20151
|
if next_step:
|
|
19526
20152
|
try:
|
|
19527
|
-
|
|
20153
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
19528
20154
|
except Exception:
|
|
19529
20155
|
pass
|
|
19530
20156
|
return True
|
|
@@ -20606,7 +21232,7 @@ class SessionState:
|
|
|
20606
21232
|
f"budget={int(self.runtime_round_budget or 0)}.\n\n"
|
|
20607
21233
|
f"{dims_ctx}"
|
|
20608
21234
|
f"{skills_ctx}"
|
|
20609
|
-
f"Workspace root: {self.files_root}\n"
|
|
21235
|
+
f"Workspace root: \"{self.files_root}\" ($SESSION_ROOT)\n"
|
|
20610
21236
|
"Infer scale_preference by semantics (fast/balanced/thorough). "
|
|
20611
21237
|
"When user preference is clear, prioritize it over your default plan. "
|
|
20612
21238
|
"Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
|
|
@@ -22384,14 +23010,13 @@ class SessionState:
|
|
|
22384
23010
|
"round_budget": int(round_budget),
|
|
22385
23011
|
"remaining_rounds": int(remaining_rounds),
|
|
22386
23012
|
}
|
|
22387
|
-
# advance_plan_step:
|
|
22388
|
-
|
|
22389
|
-
#
|
|
22390
|
-
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
|
|
22394
|
-
)
|
|
23013
|
+
# advance_plan_step: only trust semantics from what the agent actually wrote,
|
|
23014
|
+
# NOT from the manager's own route JSON — the manager often sets this flag
|
|
23015
|
+
# simultaneously with dispatching the work, advancing the step before it runs.
|
|
23016
|
+
should_advance = self._instruction_implies_step_advance(
|
|
23017
|
+
str(route.get("instruction", "") or ""),
|
|
23018
|
+
str(route.get("reason", "") or ""),
|
|
23019
|
+
)
|
|
22395
23020
|
if should_advance:
|
|
22396
23021
|
self._advance_plan_step(
|
|
22397
23022
|
evidence=trim(str(route.get("instruction", "") or ""), 200),
|
|
@@ -23391,19 +24016,32 @@ class SessionState:
|
|
|
23391
24016
|
|
|
23392
24017
|
def _agent_role_system_prompt(self, role: str) -> str:
|
|
23393
24018
|
role_key = self._sanitize_agent_role(role) or "developer"
|
|
23394
|
-
|
|
24019
|
+
skills_block = self._skills_awareness_block(for_role=role_key)
|
|
23395
24020
|
code_note = self._runtime_code_reference_prompt_block(max_chars=2600)
|
|
23396
24021
|
base = (
|
|
23397
24022
|
f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
|
|
23398
|
-
f"Workspace: {self.files_root}. Use relative paths. "
|
|
24023
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). Use relative paths or $SESSION_ROOT in bash. "
|
|
24024
|
+
f"Offline JS libraries root: $JS_LIB_ROOT. "
|
|
24025
|
+
f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
|
|
24026
|
+
f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS) or pptxgen.bundle.js (browser). "
|
|
24027
|
+
f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
|
|
23399
24028
|
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
23400
24029
|
"Keep outputs concise and action-oriented. "
|
|
23401
|
-
f"{
|
|
24030
|
+
f"{code_note + ' ' if code_note else ''}"
|
|
23402
24031
|
f"{_detect_os_shell_instruction()} "
|
|
23403
24032
|
f"{model_language_instruction(self.ui_language)} "
|
|
23404
24033
|
)
|
|
24034
|
+
mm_note = self._multimodal_capability_block()
|
|
24035
|
+
if mm_note:
|
|
24036
|
+
base = base + mm_note
|
|
24037
|
+
base = base + skills_block
|
|
23405
24038
|
if role_key == "explorer":
|
|
23406
|
-
return base +
|
|
24039
|
+
return base + (
|
|
24040
|
+
"Role: analyze goals, inspect codebase, produce research notes. "
|
|
24041
|
+
"For factual or background questions on any topic, FIRST call "
|
|
24042
|
+
"query_knowledge_library(query='<topic>', top_k=8, route='hybrid') to retrieve relevant documents. "
|
|
24043
|
+
"Prefer read/search tools. "
|
|
24044
|
+
)
|
|
23407
24045
|
if role_key == "reviewer":
|
|
23408
24046
|
if bool(self.reviewer_debug_mode):
|
|
23409
24047
|
debug_ctx = trim(str(self.reviewer_debug_context or ""), 500)
|
|
@@ -23436,6 +24074,11 @@ class SessionState:
|
|
|
23436
24074
|
)
|
|
23437
24075
|
return base + (
|
|
23438
24076
|
"Role: implement code changes, execute tools, record progress to blackboard. "
|
|
24077
|
+
"SKILL PRIORITY (critical): When ACTIVE SKILLS are listed above, find the "
|
|
24078
|
+
"<loaded-skill> messages in your context and READ them before starting any step. "
|
|
24079
|
+
"The skill's workflow, tools, and file structure OVERRIDE the plan's implementation "
|
|
24080
|
+
"approach — if the plan says 'use python-pptx' but the skill says 'use PptxGenJS', "
|
|
24081
|
+
"use PptxGenJS. The skill defines HOW to implement; the plan defines WHAT to do. "
|
|
23439
24082
|
"TODO TRACKING (mandatory): "
|
|
23440
24083
|
"After completing each logical step, call TodoWrite to update progress — "
|
|
23441
24084
|
"mark completed items as 'completed' and set the next item to 'in_progress'. "
|
|
@@ -24389,7 +25032,8 @@ class SessionState:
|
|
|
24389
25032
|
isinstance(it, dict) and str(it.get("status", it.get("state", ""))).lower() in {"completed", "done", "finished", "finish"}
|
|
24390
25033
|
for it in new_items
|
|
24391
25034
|
):
|
|
24392
|
-
self._refresh_loaded_skills_for_execution_focus(trigger="step-completed")
|
|
25035
|
+
self._refresh_loaded_skills_for_execution_focus(trigger="step-completed") # noqa: removed
|
|
25036
|
+
pass # Skills are loaded on-demand by the model
|
|
24393
25037
|
except Exception:
|
|
24394
25038
|
pass
|
|
24395
25039
|
return result
|
|
@@ -24397,7 +25041,7 @@ class SessionState:
|
|
|
24397
25041
|
result = self._todo_write_rescue(args)
|
|
24398
25042
|
# Also recheck skills on rescue write (likely a recovery situation)
|
|
24399
25043
|
try:
|
|
24400
|
-
|
|
25044
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
24401
25045
|
except Exception:
|
|
24402
25046
|
pass
|
|
24403
25047
|
return result
|
|
@@ -24848,8 +25492,6 @@ class SessionState:
|
|
|
24848
25492
|
"</live-user-adjustment>"
|
|
24849
25493
|
)
|
|
24850
25494
|
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
25495
|
self.runtime_reclassify_goal = trim(content, 4000)
|
|
24854
25496
|
# Only trigger reclassification in auto mode (no user override)
|
|
24855
25497
|
if int(getattr(self, 'user_task_level_override', 0) or 0) > 0:
|
|
@@ -24863,6 +25505,7 @@ class SessionState:
|
|
|
24863
25505
|
"weight": weight,
|
|
24864
25506
|
"priority": priority,
|
|
24865
25507
|
"applied": applied,
|
|
25508
|
+
"content": content,
|
|
24866
25509
|
}
|
|
24867
25510
|
)
|
|
24868
25511
|
row["applied_count"] = applied
|
|
@@ -24875,6 +25518,8 @@ class SessionState:
|
|
|
24875
25518
|
self.updated_at = now_ts()
|
|
24876
25519
|
self._persist()
|
|
24877
25520
|
for item in injected:
|
|
25521
|
+
# Merge user feedback with plan direction (outside lock — may do LLM call)
|
|
25522
|
+
self._merge_user_feedback_with_plan(item["content"])
|
|
24878
25523
|
self._emit(
|
|
24879
25524
|
"status",
|
|
24880
25525
|
{
|
|
@@ -24887,6 +25532,49 @@ class SessionState:
|
|
|
24887
25532
|
)
|
|
24888
25533
|
return len(injected)
|
|
24889
25534
|
|
|
25535
|
+
def _user_feedback_conflict_score(self, user_text: str, step_desc: str = "") -> float:
|
|
25536
|
+
"""Score 0.0–1.0 via LLM semantic analysis: how strongly user feedback conflicts with current plan.
|
|
25537
|
+
Falls back to 0.5 on error."""
|
|
25538
|
+
if not str(user_text or "").strip():
|
|
25539
|
+
return 0.0
|
|
25540
|
+
try:
|
|
25541
|
+
ctx = [
|
|
25542
|
+
{"role": "system", "content": (
|
|
25543
|
+
"You are a semantic conflict analyzer. "
|
|
25544
|
+
"Given a user's mid-execution feedback and the current task step, "
|
|
25545
|
+
"output ONLY a JSON object: {\"score\": <float 0.0-1.0>, \"reason\": \"<brief>\"}. "
|
|
25546
|
+
"score=0.0 means fully aligned (minor tweak/clarification). "
|
|
25547
|
+
"score=1.0 means direct contradiction/override (user wants opposite direction). "
|
|
25548
|
+
"No other text."
|
|
25549
|
+
), "ts": now_ts()},
|
|
25550
|
+
{"role": "user", "content": (
|
|
25551
|
+
f"Current step: {trim(step_desc, 300) or 'unknown'}\n"
|
|
25552
|
+
f"User feedback: {trim(user_text, 500)}\n"
|
|
25553
|
+
"Output JSON only."
|
|
25554
|
+
), "ts": now_ts()},
|
|
25555
|
+
]
|
|
25556
|
+
resp = self._chat_with_same_model_retry(
|
|
25557
|
+
ctx,
|
|
25558
|
+
tools=None,
|
|
25559
|
+
system=None,
|
|
25560
|
+
max_tokens=80,
|
|
25561
|
+
think=False,
|
|
25562
|
+
stream_thinking=False,
|
|
25563
|
+
context_label="feedback-conflict-score",
|
|
25564
|
+
retries=1,
|
|
25565
|
+
)
|
|
25566
|
+
text = str(resp.get("content", "") or "").strip()
|
|
25567
|
+
# Extract JSON from response
|
|
25568
|
+
import re as _re
|
|
25569
|
+
m = _re.search(r'\{[^}]+\}', text)
|
|
25570
|
+
if m:
|
|
25571
|
+
parsed = parse_json_object(m.group(0), {})
|
|
25572
|
+
score = float(parsed.get("score", 0.5) or 0.5)
|
|
25573
|
+
return max(0.0, min(1.0, score))
|
|
25574
|
+
except Exception:
|
|
25575
|
+
pass
|
|
25576
|
+
return 0.5
|
|
25577
|
+
|
|
24890
25578
|
def _merge_user_feedback_with_plan(self, user_text: str):
|
|
24891
25579
|
"""When user provides feedback during execution, inject plan-aware merge note into manager context."""
|
|
24892
25580
|
bb = self._ensure_blackboard()
|
|
@@ -24900,22 +25588,41 @@ class SessionState:
|
|
|
24900
25588
|
break
|
|
24901
25589
|
step_desc = trim(str(current_step.get("content", "") if current_step else "none"), 200)
|
|
24902
25590
|
is_plan_executing = plan.get("phase") == "executing"
|
|
25591
|
+
conflict_score = self._user_feedback_conflict_score(user_text, step_desc)
|
|
25592
|
+
if conflict_score >= 0.8:
|
|
25593
|
+
conflict_level = "HIGH"
|
|
25594
|
+
directive = (
|
|
25595
|
+
"USER OVERRIDE: This feedback DIRECTLY CONFLICTS with the current plan step. "
|
|
25596
|
+
"You MUST prioritize the user's instruction over the original plan. "
|
|
25597
|
+
"Adjust the current step's approach immediately to comply with user's requirement. "
|
|
25598
|
+
"Do NOT continue the original approach."
|
|
25599
|
+
)
|
|
25600
|
+
elif conflict_score >= 0.5:
|
|
25601
|
+
conflict_level = "MEDIUM"
|
|
25602
|
+
directive = (
|
|
25603
|
+
"This feedback modifies the current approach. "
|
|
25604
|
+
"Re-evaluate the current step and adjust delegation to incorporate user's requirement. "
|
|
25605
|
+
"User's direction takes precedence over plan details."
|
|
25606
|
+
)
|
|
25607
|
+
else:
|
|
25608
|
+
conflict_level = "LOW"
|
|
25609
|
+
directive = (
|
|
25610
|
+
"Minor feedback — integrate with current work direction. "
|
|
25611
|
+
"Adjust approach if needed but maintain progress."
|
|
25612
|
+
)
|
|
24903
25613
|
if is_plan_executing:
|
|
24904
25614
|
merge_note = (
|
|
24905
|
-
f"<user-feedback-merge>\n"
|
|
25615
|
+
f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
|
|
24906
25616
|
f"User provided new input during plan execution: {trim(user_text, 500)}\n"
|
|
24907
25617
|
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"
|
|
25618
|
+
f"{directive}\n"
|
|
24911
25619
|
f"</user-feedback-merge>"
|
|
24912
25620
|
)
|
|
24913
25621
|
else:
|
|
24914
25622
|
merge_note = (
|
|
24915
|
-
f"<user-feedback-merge>\n"
|
|
25623
|
+
f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
|
|
24916
25624
|
f"User provided new input: {trim(user_text, 500)}\n"
|
|
24917
|
-
f"
|
|
24918
|
-
f"Adjust approach if needed but maintain progress.\n"
|
|
25625
|
+
f"{directive}\n"
|
|
24919
25626
|
f"</user-feedback-merge>"
|
|
24920
25627
|
)
|
|
24921
25628
|
if self._is_multi_agent_mode():
|
|
@@ -24925,6 +25632,13 @@ class SessionState:
|
|
|
24925
25632
|
"ts": now_ts(),
|
|
24926
25633
|
"agent_role": "manager",
|
|
24927
25634
|
})
|
|
25635
|
+
else:
|
|
25636
|
+
# single mode: inject directly into message history so the model sees it
|
|
25637
|
+
self.messages.append({
|
|
25638
|
+
"role": "user",
|
|
25639
|
+
"content": merge_note,
|
|
25640
|
+
"ts": now_ts(),
|
|
25641
|
+
})
|
|
24928
25642
|
|
|
24929
25643
|
def _is_restart_scenario(self) -> bool:
|
|
24930
25644
|
"""Check if current state is a restart after finished/aborted task."""
|
|
@@ -25006,6 +25720,10 @@ class SessionState:
|
|
|
25006
25720
|
if _awaiting_plan_choice:
|
|
25007
25721
|
# Restore plan proposal so choice can be parsed
|
|
25008
25722
|
self.runtime_plan_mode_needed = True
|
|
25723
|
+
# Reset completed plan/todo/skills blackboard state so the manager
|
|
25724
|
+
# does not see status=COMPLETED on the very first round and immediately finish.
|
|
25725
|
+
if not _awaiting_plan_choice:
|
|
25726
|
+
self._reset_blackboard_plan_state_locked()
|
|
25009
25727
|
self.run_generation = int(self.run_generation) + 1
|
|
25010
25728
|
clean_goal = trim(str(content or "").strip(), 4000)
|
|
25011
25729
|
self._refresh_runtime_code_reference(clean_goal or content)
|
|
@@ -25525,6 +26243,7 @@ class SessionState:
|
|
|
25525
26243
|
if self.stall_escalation_triggered:
|
|
25526
26244
|
self._emit("status", {"summary": "sync loop break: stall escalated to plan mode"})
|
|
25527
26245
|
break
|
|
26246
|
+
self._inject_pending_user_inputs()
|
|
25528
26247
|
self._apply_auto_compact_if_needed("auto:multi-sync")
|
|
25529
26248
|
# Periodic checkpoint in multi-agent sync loop
|
|
25530
26249
|
if rounds_used % CHECKPOINT_INTERVAL_ROUNDS == 0:
|
|
@@ -25890,7 +26609,7 @@ class SessionState:
|
|
|
25890
26609
|
|
|
25891
26610
|
# Auto-discover and load relevant skills before research
|
|
25892
26611
|
try:
|
|
25893
|
-
|
|
26612
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
25894
26613
|
except Exception:
|
|
25895
26614
|
pass
|
|
25896
26615
|
|
|
@@ -25900,6 +26619,7 @@ class SessionState:
|
|
|
25900
26619
|
for r in range(PLAN_MODE_EXPLORER_MAX_ROUNDS):
|
|
25901
26620
|
if self.cancel_requested:
|
|
25902
26621
|
return
|
|
26622
|
+
self._inject_pending_user_inputs()
|
|
25903
26623
|
step = self._plan_mode_explorer_turn(pinned_selection, round_idx=r)
|
|
25904
26624
|
if step.get("status") in ("no-tools", "skip", "interrupted"):
|
|
25905
26625
|
break
|
|
@@ -25973,33 +26693,38 @@ class SessionState:
|
|
|
25973
26693
|
f"## User Request\n{goal}\n\n"
|
|
25974
26694
|
f"{skills_section}"
|
|
25975
26695
|
f"## Instructions\n"
|
|
25976
|
-
f"1.
|
|
25977
|
-
f"
|
|
25978
|
-
f"
|
|
25979
|
-
f"
|
|
25980
|
-
f"4.
|
|
25981
|
-
f"
|
|
25982
|
-
f"
|
|
25983
|
-
f"
|
|
26696
|
+
f"1. Call `list_skills` FIRST to discover available skills — identify which skills are relevant "
|
|
26697
|
+
f"to this task and note their names and capabilities in your findings.\n"
|
|
26698
|
+
f"2. List all uploaded/workspace files with `ls uploaded/` or `ls` to know what inputs are available\n"
|
|
26699
|
+
f"3. Read uploaded files (.parsed.md preferred over .pdf) to understand their content and structure\n"
|
|
26700
|
+
f"4. If relevant skills exist, call `load_skill` to load the most relevant one and analyze its "
|
|
26701
|
+
f"workflow steps, scripts, tools, and file paths\n"
|
|
26702
|
+
f"5. Identify key technical details, data points, and structure needed for the output\n"
|
|
26703
|
+
f"6. Assess risks and note any ambiguities that need user input\n"
|
|
26704
|
+
f"7. DO NOT write, edit, or create any files. Read-only analysis only.\n"
|
|
26705
|
+
f"8. Write your findings to the blackboard under 'plan_findings'. Include:\n"
|
|
26706
|
+
f" - Relevant skills found (names, what they do, how to invoke them)\n"
|
|
25984
26707
|
f" - File inventory (uploaded files, their types, sizes, key content)\n"
|
|
25985
|
-
f" - Skill workflow breakdown (concrete tools, scripts, paths for each
|
|
26708
|
+
f" - Skill workflow breakdown (concrete tools, scripts, paths for each relevant skill)\n"
|
|
25986
26709
|
f" - Content analysis (key themes, structure, data points extracted from inputs)\n\n"
|
|
25987
|
-
f"Workspace: {self.files_root}\n"
|
|
26710
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT)\n"
|
|
25988
26711
|
f"{os_note}\n"
|
|
25989
26712
|
f"{lang_note}"
|
|
25990
26713
|
)
|
|
25991
26714
|
|
|
25992
26715
|
def _seed_plan_mode_explorer_context(self, research_prompt: str):
|
|
25993
26716
|
os_note = _detect_os_shell_instruction()
|
|
25994
|
-
|
|
26717
|
+
skills_block = self._skills_awareness_block(for_role="explorer")
|
|
25995
26718
|
self._append_agent_context_message("explorer", {
|
|
25996
26719
|
"role": "system",
|
|
25997
26720
|
"content": (
|
|
25998
26721
|
"You are Explorer in plan-mode (read-only research). "
|
|
25999
26722
|
"Analyze the codebase to understand the task scope. "
|
|
26000
26723
|
"Do NOT modify any files. Use read_file, bash (read-only commands), "
|
|
26001
|
-
"and blackboard tools only. "
|
|
26002
|
-
f"{
|
|
26724
|
+
"list_skills, load_skill, and blackboard tools only. "
|
|
26725
|
+
f"{skills_block}"
|
|
26726
|
+
"IMPORTANT: If the task requires specialized output (PPTX, reports, deep research, code review), "
|
|
26727
|
+
"call list_skills first to discover relevant skills, then note in plan_findings which skills to use. "
|
|
26003
26728
|
f"{os_note} "
|
|
26004
26729
|
f"{model_language_instruction(self.ui_language)}"
|
|
26005
26730
|
),
|
|
@@ -26030,16 +26755,16 @@ class SessionState:
|
|
|
26030
26755
|
self.current_phase = f"plan-mode:explorer:round-{round_idx}"
|
|
26031
26756
|
self.current_tool_name = ""
|
|
26032
26757
|
self.active_agent_role = "explorer"
|
|
26033
|
-
# Build
|
|
26034
|
-
|
|
26758
|
+
# Build skills awareness block (same as sync/single mode)
|
|
26759
|
+
skills_block = self._skills_awareness_block(for_role="explorer")
|
|
26035
26760
|
response = self._chat_with_same_model_retry(
|
|
26036
26761
|
ctx,
|
|
26037
26762
|
tools=filtered_tools,
|
|
26038
26763
|
system=(
|
|
26039
26764
|
"You are Explorer in plan-mode research. Read-only analysis. "
|
|
26040
26765
|
"Do NOT create, write, or edit files. "
|
|
26041
|
-
f"Workspace: {self.files_root}. "
|
|
26042
|
-
f"{
|
|
26766
|
+
f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
|
|
26767
|
+
f"{skills_block}"
|
|
26043
26768
|
f"{_detect_os_shell_instruction()} "
|
|
26044
26769
|
f"{model_language_instruction(self.ui_language)}"
|
|
26045
26770
|
),
|
|
@@ -26448,7 +27173,8 @@ class SessionState:
|
|
|
26448
27173
|
synthesis_ctx = [
|
|
26449
27174
|
{"role": "system", "content": (
|
|
26450
27175
|
"You are a technical architect synthesizing research into actionable plans. "
|
|
26451
|
-
"When
|
|
27176
|
+
"When skills are referenced in the findings, incorporate their actual workflow steps into plan options. "
|
|
27177
|
+
f"{self._skills_awareness_block(for_role='developer')}"
|
|
26452
27178
|
), "ts": now_ts()},
|
|
26453
27179
|
{"role": "user", "content": synthesis_prompt, "ts": now_ts()},
|
|
26454
27180
|
]
|
|
@@ -26691,7 +27417,7 @@ class SessionState:
|
|
|
26691
27417
|
except Exception:
|
|
26692
27418
|
pass
|
|
26693
27419
|
try:
|
|
26694
|
-
|
|
27420
|
+
pass # Skills are loaded on-demand by the model via load_skill
|
|
26695
27421
|
except Exception:
|
|
26696
27422
|
pass
|
|
26697
27423
|
# Pre-load skills explicitly mentioned in plan steps
|
|
@@ -26755,13 +27481,13 @@ class SessionState:
|
|
|
26755
27481
|
)
|
|
26756
27482
|
},
|
|
26757
27483
|
)
|
|
26758
|
-
# ──
|
|
27484
|
+
# ── Skills are loaded on-demand by the model via load_skill ──
|
|
26759
27485
|
try:
|
|
26760
27486
|
self._emit(
|
|
26761
27487
|
"status",
|
|
26762
|
-
{"summary": "
|
|
27488
|
+
{"summary": "skills available on-demand"},
|
|
26763
27489
|
)
|
|
26764
|
-
|
|
27490
|
+
pass # No automatic pre-classify skill discovery
|
|
26765
27491
|
except Exception:
|
|
26766
27492
|
pass
|
|
26767
27493
|
initial_policy_media_inputs = self._recent_multimodal_inputs()
|
|
@@ -30581,16 +31307,32 @@ function _mathRunTypeset(root,key=''){
|
|
|
30581
31307
|
if(!root)return;
|
|
30582
31308
|
const k=String(key||'').trim();
|
|
30583
31309
|
if(k&&root.getAttribute('data-math-key')===k)return;
|
|
31310
|
+
const mathJaxCandidates=[
|
|
31311
|
+
'/assets/js_lib/tex-mml-chtml.js',
|
|
31312
|
+
'/assets/js_lib/mathjax/tex-mml-chtml.js',
|
|
31313
|
+
'/assets/js_lib/es5/tex-mml-chtml.js',
|
|
31314
|
+
'/assets/js_lib/mathjax/es5/tex-mml-chtml.js',
|
|
31315
|
+
'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
|
|
31316
|
+
];
|
|
31317
|
+
const loadMathJax=(idx=0)=>{
|
|
31318
|
+
const src=String(mathJaxCandidates[idx]||'').trim();
|
|
31319
|
+
if(!src)return;
|
|
31320
|
+
const s=document.createElement('script');
|
|
31321
|
+
s.src=src;
|
|
31322
|
+
s.async=true;
|
|
31323
|
+
s.dataset.mathjaxCandidate=String(idx);
|
|
31324
|
+
s.onerror=()=>{
|
|
31325
|
+
if(idx+1<mathJaxCandidates.length)loadMathJax(idx+1);
|
|
31326
|
+
};
|
|
31327
|
+
document.head.appendChild(s);
|
|
31328
|
+
};
|
|
30584
31329
|
const run=(retry)=>{
|
|
30585
31330
|
const mj=window.MathJax;
|
|
30586
31331
|
if(!mj||typeof mj.typesetPromise!=='function'){
|
|
30587
31332
|
// Lazy-load MathJax on first actual math demand
|
|
30588
31333
|
if(!window._mjaxLoading){
|
|
30589
31334
|
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);
|
|
31335
|
+
loadMathJax(0);
|
|
30594
31336
|
}
|
|
30595
31337
|
if(retry<20)setTimeout(()=>run(retry+1),200);
|
|
30596
31338
|
return;
|
|
@@ -34349,7 +35091,7 @@ class RAGContentParser:
|
|
|
34349
35091
|
|
|
34350
35092
|
text = extract_text(str(pdf_path))
|
|
34351
35093
|
if text and text.strip():
|
|
34352
|
-
return trim(text.strip(),
|
|
35094
|
+
return trim(text.strip(), 800_000)
|
|
34353
35095
|
except ImportError:
|
|
34354
35096
|
pass
|
|
34355
35097
|
except Exception:
|
|
@@ -34364,7 +35106,7 @@ class RAGContentParser:
|
|
|
34364
35106
|
timeout=60,
|
|
34365
35107
|
)
|
|
34366
35108
|
if r.returncode == 0 and r.stdout.strip():
|
|
34367
|
-
return trim(r.stdout.strip(),
|
|
35109
|
+
return trim(r.stdout.strip(), 800_000)
|
|
34368
35110
|
except Exception:
|
|
34369
35111
|
pass
|
|
34370
35112
|
try:
|
|
@@ -36525,7 +37267,7 @@ class RAGLibraryStore:
|
|
|
36525
37267
|
backup_path.write_bytes(raw_bytes)
|
|
36526
37268
|
elif source_fp and source_fp.exists():
|
|
36527
37269
|
shutil.copy2(source_fp, backup_path)
|
|
36528
|
-
semantic_text = trim(str(parse_result.get("text", "") or ""),
|
|
37270
|
+
semantic_text = trim(str(parse_result.get("text", "") or ""), 800_000)
|
|
36529
37271
|
multimodal_row = dict(multimodal or {})
|
|
36530
37272
|
mm_summary = trim(str(multimodal_row.get("summary", "") or ""), 2400)
|
|
36531
37273
|
mm_tags = [str(x).strip() for x in (multimodal_row.get("tags", []) or []) if str(x).strip()]
|
|
@@ -39952,20 +40694,12 @@ class AppContext:
|
|
|
39952
40694
|
self.base_url = base_url
|
|
39953
40695
|
self.model = model
|
|
39954
40696
|
self.thinking = False
|
|
39955
|
-
self.js_lib_root = offline_js_lib_root(
|
|
40697
|
+
self.js_lib_root = offline_js_lib_root(SCRIPT_DIR)
|
|
39956
40698
|
self.offline_js_summary: dict = {}
|
|
39957
40699
|
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
|
-
}
|
|
40700
|
+
self.offline_js_summary = load_offline_js_lib_index(self.js_lib_root)
|
|
40701
|
+
except Exception:
|
|
40702
|
+
self.offline_js_summary = {}
|
|
39969
40703
|
self.default_language = normalize_ui_language(default_language)
|
|
39970
40704
|
self.ui_style = normalize_ui_style(ui_style)
|
|
39971
40705
|
self.context_token_limit = max(
|
|
@@ -40264,16 +40998,7 @@ class AppContext:
|
|
|
40264
40998
|
return CODE_ADMIN_JS
|
|
40265
40999
|
|
|
40266
41000
|
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
|
|
41001
|
+
return _resolve_js_lib_asset_path(self.js_lib_root, str(filename or "").strip())
|
|
40277
41002
|
|
|
40278
41003
|
def rag_three_asset_info(self) -> dict:
|
|
40279
41004
|
picks = [
|
|
@@ -41071,7 +41796,7 @@ class AppContext:
|
|
|
41071
41796
|
f"{str(row.get('title', '') or '').strip()} "
|
|
41072
41797
|
f"score={str(row.get('score', 0) or 0)}"
|
|
41073
41798
|
)
|
|
41074
|
-
snippet = trim(str(row.get("text", "") or ""),
|
|
41799
|
+
snippet = trim(str(row.get("text", "") or ""), 800)
|
|
41075
41800
|
if snippet:
|
|
41076
41801
|
lines.append(snippet)
|
|
41077
41802
|
return "\n".join(lines)
|
|
@@ -43150,8 +43875,8 @@ class RagAdminHandler(BaseHTTPRequestHandler):
|
|
|
43150
43875
|
if path == "/assets/rag-admin.js":
|
|
43151
43876
|
return self._send_text(self.app.web_ui_rag_admin_js(), "application/javascript; charset=utf-8")
|
|
43152
43877
|
if path.startswith("/assets/js_lib/"):
|
|
43153
|
-
|
|
43154
|
-
fp = self.app.rag_js_lib_asset_path(
|
|
43878
|
+
asset_ref = path[len("/assets/js_lib/"):]
|
|
43879
|
+
fp = self.app.rag_js_lib_asset_path(asset_ref)
|
|
43155
43880
|
if not fp:
|
|
43156
43881
|
return self._send_json({"error": "asset not found"}, status=404)
|
|
43157
43882
|
try:
|
|
@@ -43159,7 +43884,7 @@ class RagAdminHandler(BaseHTTPRequestHandler):
|
|
|
43159
43884
|
except Exception as exc:
|
|
43160
43885
|
return self._send_json({"error": str(exc)}, status=500)
|
|
43161
43886
|
content_type = guess_mime_from_name(fp.name, "application/javascript")
|
|
43162
|
-
if fp.suffix.lower()
|
|
43887
|
+
if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
|
|
43163
43888
|
content_type = "application/javascript; charset=utf-8"
|
|
43164
43889
|
return self._send_inline_bytes(data, content_type)
|
|
43165
43890
|
if path == "/api/health":
|
|
@@ -43307,8 +44032,8 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
|
|
|
43307
44032
|
if path == "/assets/code-admin.js":
|
|
43308
44033
|
return self._send_text(self.app.web_ui_code_admin_js(), "application/javascript; charset=utf-8")
|
|
43309
44034
|
if path.startswith("/assets/js_lib/"):
|
|
43310
|
-
|
|
43311
|
-
fp = self.app.rag_js_lib_asset_path(
|
|
44035
|
+
asset_ref = path[len("/assets/js_lib/"):]
|
|
44036
|
+
fp = self.app.rag_js_lib_asset_path(asset_ref)
|
|
43312
44037
|
if not fp:
|
|
43313
44038
|
return self._send_json({"error": "asset not found"}, status=404)
|
|
43314
44039
|
try:
|
|
@@ -43316,7 +44041,7 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
|
|
|
43316
44041
|
except Exception as exc:
|
|
43317
44042
|
return self._send_json({"error": str(exc)}, status=500)
|
|
43318
44043
|
content_type = guess_mime_from_name(fp.name, "application/javascript")
|
|
43319
|
-
if fp.suffix.lower()
|
|
44044
|
+
if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
|
|
43320
44045
|
content_type = "application/javascript; charset=utf-8"
|
|
43321
44046
|
return self._send_inline_bytes(data, content_type)
|
|
43322
44047
|
if path == "/api/health":
|
|
@@ -43938,6 +44663,17 @@ def main():
|
|
|
43938
44663
|
except Exception as exc:
|
|
43939
44664
|
print(f"[web-agent] failed to apply --config: {exc}")
|
|
43940
44665
|
sys.exit(2)
|
|
44666
|
+
# JS lib download (default on; set download_js_lib: false in --config to disable)
|
|
44667
|
+
_js_dl_enabled = extract_js_lib_download_setting(external_config)
|
|
44668
|
+
if _js_dl_enabled is None:
|
|
44669
|
+
_js_dl_enabled = True
|
|
44670
|
+
if _js_dl_enabled:
|
|
44671
|
+
try:
|
|
44672
|
+
app.offline_js_summary = ensure_offline_js_libs(
|
|
44673
|
+
app.workspace, force=False, verbose=True, no_connection_deadline=60.0
|
|
44674
|
+
)
|
|
44675
|
+
except Exception as _js_exc:
|
|
44676
|
+
print(f"[js_lib] download error: {_js_exc}")
|
|
43941
44677
|
web_ui_state = app.configure_web_ui(
|
|
43942
44678
|
config_path=str(web_ui_config_path),
|
|
43943
44679
|
ui_dir=resolved_web_ui_dir,
|