clouds-coder 2026.3.8__tar.gz → 2026.3.17__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.8 → clouds_coder-2026.3.17}/Clouds_Coder.py +1504 -328
- {clouds_coder-2026.3.8/clouds_coder.egg-info → clouds_coder-2026.3.17}/PKG-INFO +4 -8
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/README.md +3 -7
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17/clouds_coder.egg-info}/PKG-INFO +4 -8
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/pyproject.toml +1 -1
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/LICENSE +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/setup.cfg +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.17}/tests/test_smoke.py +0 -0
|
@@ -71,9 +71,12 @@ DEFAULT_TIMEOUT_SECONDS = max(
|
|
|
71
71
|
)
|
|
72
72
|
DEFAULT_REQUEST_TIMEOUT = DEFAULT_TIMEOUT_SECONDS
|
|
73
73
|
AUTO_CONTINUE_BUDGET_DEFAULT = 30
|
|
74
|
-
AGENT_MAX_OUTPUT_TOKENS =
|
|
74
|
+
AGENT_MAX_OUTPUT_TOKENS = 16384
|
|
75
|
+
OLLAMA_THINKING_TOOL_BUFFER = 4096
|
|
75
76
|
WATCHDOG_INTENT_NO_TOOL_THRESHOLD = 2
|
|
76
77
|
WATCHDOG_REPEAT_NO_TOOL_THRESHOLD = 2
|
|
78
|
+
WATCHDOG_INTENT_NO_TOOL_THRESHOLD_SINGLE = 4
|
|
79
|
+
WATCHDOG_REPEAT_NO_TOOL_THRESHOLD_SINGLE = 4
|
|
77
80
|
WATCHDOG_STATE_STALL_THRESHOLD = 6
|
|
78
81
|
WATCHDOG_CONTEXT_STALL_THRESHOLD = 2
|
|
79
82
|
WATCHDOG_REPEAT_SIMILARITY_THRESHOLD = 0.85
|
|
@@ -202,7 +205,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
202
205
|
"execution_mode": EXECUTION_MODE_SINGLE,
|
|
203
206
|
"participants": ["developer"],
|
|
204
207
|
"assigned_expert": "developer",
|
|
205
|
-
"round_budget":
|
|
208
|
+
"round_budget": 2,
|
|
206
209
|
"requires_user_confirmation": False,
|
|
207
210
|
"complexity": "simple",
|
|
208
211
|
},
|
|
@@ -211,7 +214,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
211
214
|
"execution_mode": EXECUTION_MODE_SINGLE,
|
|
212
215
|
"participants": ["developer"],
|
|
213
216
|
"assigned_expert": "developer",
|
|
214
|
-
"round_budget":
|
|
217
|
+
"round_budget": 6,
|
|
215
218
|
"requires_user_confirmation": False,
|
|
216
219
|
"complexity": "simple",
|
|
217
220
|
},
|
|
@@ -220,7 +223,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
220
223
|
"execution_mode": EXECUTION_MODE_SYNC,
|
|
221
224
|
"participants": ["explorer", "developer"],
|
|
222
225
|
"assigned_expert": "developer",
|
|
223
|
-
"round_budget":
|
|
226
|
+
"round_budget": 10,
|
|
224
227
|
"requires_user_confirmation": False,
|
|
225
228
|
"complexity": "simple",
|
|
226
229
|
},
|
|
@@ -229,7 +232,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
229
232
|
"execution_mode": EXECUTION_MODE_SYNC,
|
|
230
233
|
"participants": ["explorer", "developer", "reviewer"],
|
|
231
234
|
"assigned_expert": "developer",
|
|
232
|
-
"round_budget":
|
|
235
|
+
"round_budget": 24,
|
|
233
236
|
"requires_user_confirmation": False,
|
|
234
237
|
"complexity": "complex",
|
|
235
238
|
},
|
|
@@ -485,6 +488,30 @@ CODE_PREVIEW_EXTS = {
|
|
|
485
488
|
".svelte",
|
|
486
489
|
".gradle",
|
|
487
490
|
".properties",
|
|
491
|
+
# Fortran
|
|
492
|
+
".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
|
|
493
|
+
# Haskell
|
|
494
|
+
".hs", ".lhs",
|
|
495
|
+
# Erlang / Elixir
|
|
496
|
+
".erl", ".hrl", ".ex", ".exs",
|
|
497
|
+
# OCaml
|
|
498
|
+
".ml", ".mli",
|
|
499
|
+
# HDL
|
|
500
|
+
".vhd", ".vhdl", ".v", ".sv",
|
|
501
|
+
# Assembly
|
|
502
|
+
".asm", ".s",
|
|
503
|
+
# Infra / Schema
|
|
504
|
+
".proto", ".tf", ".tfvars", ".prisma", ".graphql", ".gql",
|
|
505
|
+
# Modern systems
|
|
506
|
+
".zig", ".nim", ".jl", ".cr", ".d",
|
|
507
|
+
# Lisp family
|
|
508
|
+
".clj", ".cljs", ".cljc", ".lisp", ".cl", ".el", ".rkt",
|
|
509
|
+
# Pascal
|
|
510
|
+
".pas", ".pp",
|
|
511
|
+
# Shader
|
|
512
|
+
".wgsl", ".glsl", ".hlsl",
|
|
513
|
+
# Misc
|
|
514
|
+
".groovy", ".cmake", ".dockerfile",
|
|
488
515
|
}
|
|
489
516
|
CODE_PREVIEW_FILENAMES = {
|
|
490
517
|
"dockerfile",
|
|
@@ -1310,6 +1337,117 @@ def trim(text: object, limit: int = MAX_TOOL_OUTPUT) -> str:
|
|
|
1310
1337
|
s = str(text)
|
|
1311
1338
|
return s if len(s) <= limit else s[:limit] + "\n...(truncated)"
|
|
1312
1339
|
|
|
1340
|
+
|
|
1341
|
+
def _fmt_export_ts(ts: float | int) -> str:
|
|
1342
|
+
v = float(ts or 0)
|
|
1343
|
+
if v <= 0:
|
|
1344
|
+
return ""
|
|
1345
|
+
try:
|
|
1346
|
+
from datetime import datetime
|
|
1347
|
+
return datetime.fromtimestamp(v).strftime("%Y-%m-%d %H:%M:%S")
|
|
1348
|
+
except Exception:
|
|
1349
|
+
return ""
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _html_esc(text: str) -> str:
|
|
1353
|
+
return html.escape(str(text or ""))
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _text_to_minimal_pdf(text: str) -> bytes:
|
|
1357
|
+
"""纯 Python 最小 PDF 生成器,无外部依赖。"""
|
|
1358
|
+
raw = str(text or "")
|
|
1359
|
+
lines = raw.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
1360
|
+
font_size = 10
|
|
1361
|
+
leading = font_size * 1.35
|
|
1362
|
+
margin_top, margin_bottom, margin_left, margin_right = 50, 50, 50, 50
|
|
1363
|
+
page_w, page_h = 595, 842 # A4
|
|
1364
|
+
usable_w = page_w - margin_left - margin_right
|
|
1365
|
+
max_chars_per_line = max(20, int(usable_w / (font_size * 0.52)))
|
|
1366
|
+
usable_h = page_h - margin_top - margin_bottom
|
|
1367
|
+
lines_per_page = max(1, int(usable_h / leading))
|
|
1368
|
+
|
|
1369
|
+
# 折行
|
|
1370
|
+
wrapped: list[str] = []
|
|
1371
|
+
for line in lines:
|
|
1372
|
+
if not line:
|
|
1373
|
+
wrapped.append("")
|
|
1374
|
+
continue
|
|
1375
|
+
while len(line) > max_chars_per_line:
|
|
1376
|
+
wrapped.append(line[:max_chars_per_line])
|
|
1377
|
+
line = line[max_chars_per_line:]
|
|
1378
|
+
wrapped.append(line)
|
|
1379
|
+
|
|
1380
|
+
# 分页
|
|
1381
|
+
pages: list[list[str]] = []
|
|
1382
|
+
for i in range(0, len(wrapped), lines_per_page):
|
|
1383
|
+
pages.append(wrapped[i:i + lines_per_page])
|
|
1384
|
+
if not pages:
|
|
1385
|
+
pages = [[""]]
|
|
1386
|
+
|
|
1387
|
+
def _pdf_escape(s: str) -> str:
|
|
1388
|
+
return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
|
1389
|
+
|
|
1390
|
+
def _safe_latin1(s: str) -> str:
|
|
1391
|
+
return s.encode("latin-1", errors="replace").decode("latin-1")
|
|
1392
|
+
|
|
1393
|
+
objects: list[bytes] = []
|
|
1394
|
+
offsets: list[int] = []
|
|
1395
|
+
buf = b"%PDF-1.4\n"
|
|
1396
|
+
|
|
1397
|
+
def add_obj(content: str) -> int:
|
|
1398
|
+
nonlocal buf
|
|
1399
|
+
idx = len(objects) + 1
|
|
1400
|
+
offsets.append(len(buf))
|
|
1401
|
+
obj_bytes = f"{idx} 0 obj\n{content}\nendobj\n".encode("latin-1")
|
|
1402
|
+
buf += obj_bytes
|
|
1403
|
+
objects.append(obj_bytes)
|
|
1404
|
+
return idx
|
|
1405
|
+
|
|
1406
|
+
catalog_id = add_obj("<< /Type /Catalog /Pages 2 0 R >>")
|
|
1407
|
+
add_obj("PAGES_PLACEHOLDER") # obj 2: placeholder, replaced after page generation
|
|
1408
|
+
font_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>")
|
|
1409
|
+
|
|
1410
|
+
page_ids: list[int] = []
|
|
1411
|
+
for page_lines in pages:
|
|
1412
|
+
stream_lines = [f"BT /F1 {font_size} Tf"]
|
|
1413
|
+
y = page_h - margin_top
|
|
1414
|
+
stream_lines.append(f"{margin_left} {y} Td")
|
|
1415
|
+
for pl in page_lines:
|
|
1416
|
+
safe = _safe_latin1(_pdf_escape(pl))
|
|
1417
|
+
stream_lines.append(f"({safe}) Tj")
|
|
1418
|
+
stream_lines.append(f"0 -{leading:.1f} Td")
|
|
1419
|
+
stream_lines.append("ET")
|
|
1420
|
+
stream = "\n".join(stream_lines)
|
|
1421
|
+
stream_bytes = stream.encode("latin-1")
|
|
1422
|
+
content_id = add_obj(f"<< /Length {len(stream_bytes)} >>\nstream\n{stream}\nendstream")
|
|
1423
|
+
page_id = add_obj(
|
|
1424
|
+
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {page_w} {page_h}] "
|
|
1425
|
+
f"/Contents {content_id} 0 R /Resources << /Font << /F1 {font_id} 0 R >> >> >>"
|
|
1426
|
+
)
|
|
1427
|
+
page_ids.append(page_id)
|
|
1428
|
+
|
|
1429
|
+
kids = " ".join(f"{pid} 0 R" for pid in page_ids)
|
|
1430
|
+
pages_content = f"<< /Type /Pages /Kids [{kids}] /Count {len(page_ids)} >>"
|
|
1431
|
+
pages_bytes = f"2 0 obj\n{pages_content}\nendobj\n".encode("latin-1")
|
|
1432
|
+
old_pages = objects[1]
|
|
1433
|
+
buf = buf.replace(b"2 0 obj\nPAGES_PLACEHOLDER\nendobj\n", pages_bytes)
|
|
1434
|
+
size_diff = len(pages_bytes) - len(old_pages)
|
|
1435
|
+
for i in range(2, len(offsets)):
|
|
1436
|
+
offsets[i] += size_diff
|
|
1437
|
+
|
|
1438
|
+
xref_offset = len(buf)
|
|
1439
|
+
buf += b"xref\n"
|
|
1440
|
+
buf += f"0 {len(objects) + 1}\n".encode("latin-1")
|
|
1441
|
+
buf += b"0000000000 65535 f \n"
|
|
1442
|
+
for off in offsets:
|
|
1443
|
+
buf += f"{off:010d} 00000 n \n".encode("latin-1")
|
|
1444
|
+
buf += b"trailer\n"
|
|
1445
|
+
buf += f"<< /Size {len(objects) + 1} /Root {catalog_id} 0 R >>\n".encode("latin-1")
|
|
1446
|
+
buf += b"startxref\n"
|
|
1447
|
+
buf += f"{xref_offset}\n".encode("latin-1")
|
|
1448
|
+
buf += b"%%EOF\n"
|
|
1449
|
+
return buf
|
|
1450
|
+
|
|
1313
1451
|
def compress_text_blob(text: str) -> str:
|
|
1314
1452
|
src = str(text or "")
|
|
1315
1453
|
if not src:
|
|
@@ -2218,7 +2356,7 @@ def try_read_text(path: Path, max_bytes: int = 400_000) -> str | None:
|
|
|
2218
2356
|
except Exception:
|
|
2219
2357
|
return None
|
|
2220
2358
|
|
|
2221
|
-
def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int = 400) -> str:
|
|
2359
|
+
def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int = 400) -> tuple[str, int, int]:
|
|
2222
2360
|
old_lines = old_text.splitlines()
|
|
2223
2361
|
new_lines = new_text.splitlines()
|
|
2224
2362
|
diff = list(
|
|
@@ -2230,9 +2368,12 @@ def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int =
|
|
|
2230
2368
|
lineterm="",
|
|
2231
2369
|
)
|
|
2232
2370
|
)
|
|
2233
|
-
if
|
|
2371
|
+
added = sum(1 for ln in diff if ln.startswith("+") and not ln.startswith("+++"))
|
|
2372
|
+
deleted = sum(1 for ln in diff if ln.startswith("-") and not ln.startswith("---"))
|
|
2373
|
+
if max_lines and len(diff) > max_lines:
|
|
2234
2374
|
diff = diff[:max_lines] + [f"... diff truncated, total lines={len(diff)}"]
|
|
2235
|
-
|
|
2375
|
+
text = "\n".join(diff) if diff else f"@@ no textual diff for {path}"
|
|
2376
|
+
return text, added, deleted
|
|
2236
2377
|
|
|
2237
2378
|
def _skip_row(text: str) -> dict:
|
|
2238
2379
|
msg = str(text or "").strip() or "⋮"
|
|
@@ -3435,6 +3576,7 @@ def _module_exists(name: str) -> bool:
|
|
|
3435
3576
|
|
|
3436
3577
|
def detect_upload_parser_capabilities() -> dict:
|
|
3437
3578
|
return {
|
|
3579
|
+
"pdfminer": _module_exists("pdfminer"),
|
|
3438
3580
|
"openpyxl": _module_exists("openpyxl"),
|
|
3439
3581
|
"xlrd": _module_exists("xlrd"),
|
|
3440
3582
|
"python_docx": _module_exists("docx"),
|
|
@@ -3446,6 +3588,7 @@ def detect_upload_parser_capabilities() -> dict:
|
|
|
3446
3588
|
"catdoc": bool(shutil.which("catdoc")),
|
|
3447
3589
|
"catppt": bool(shutil.which("catppt")),
|
|
3448
3590
|
"textutil": bool(shutil.which("textutil")),
|
|
3591
|
+
"playwright": _module_exists("playwright"),
|
|
3449
3592
|
}
|
|
3450
3593
|
|
|
3451
3594
|
def _render_cap_markdown(caps: dict) -> str:
|
|
@@ -4671,6 +4814,96 @@ SKILL_PROTOCOL_SPECS = [
|
|
|
4671
4814
|
},
|
|
4672
4815
|
]
|
|
4673
4816
|
|
|
4817
|
+
# ---------------------------------------------------------------------------
|
|
4818
|
+
# Built-in skill guides (injected into SkillStore on reload)
|
|
4819
|
+
# ---------------------------------------------------------------------------
|
|
4820
|
+
_BUILTIN_SKILLS: dict[str, dict] = {
|
|
4821
|
+
"workspace-paths": {
|
|
4822
|
+
"description": "File path rules, virtual path mapping, relative path conventions",
|
|
4823
|
+
"body": (
|
|
4824
|
+
"# Workspace Paths Guide\n"
|
|
4825
|
+
"- Always use relative paths (e.g. src/main.py) for file tools.\n"
|
|
4826
|
+
"- Runtime maps relative paths to session absolute root automatically.\n"
|
|
4827
|
+
"- '/workspace/...' is a virtual alias for tool arguments only; never create OS-level /workspace.\n"
|
|
4828
|
+
"- When user references uploaded files, prioritize workspace uploads directory.\n"
|
|
4829
|
+
"- For shell commands, use relative paths or $PWD-based references.\n"
|
|
4830
|
+
),
|
|
4831
|
+
},
|
|
4832
|
+
"task-management": {
|
|
4833
|
+
"description": "Todo/Task creation, updates, and best practices",
|
|
4834
|
+
"body": (
|
|
4835
|
+
"# Task Management Guide\n"
|
|
4836
|
+
"- For level 1-2 (simple) tasks: skip todo scaffolding, give direct response.\n"
|
|
4837
|
+
"- For level 3+ tasks: call TodoWrite early with 3-7 concise items, one marked in_progress.\n"
|
|
4838
|
+
"- Update todos only when plan or status actually changes. Avoid redundant calls.\n"
|
|
4839
|
+
"- If TodoWrite fails or repeats unchanged, use TodoWriteRescue with simple string items.\n"
|
|
4840
|
+
"- Use task_create/task_update/task_list for multi-step structured work.\n"
|
|
4841
|
+
),
|
|
4842
|
+
},
|
|
4843
|
+
"tool-best-practices": {
|
|
4844
|
+
"description": "write_file vs edit_file, bash usage, error handling conventions",
|
|
4845
|
+
"body": (
|
|
4846
|
+
"# Tool Best Practices\n"
|
|
4847
|
+
"- Prefer write_file/edit_file for code changes (UI renders line-level diffs).\n"
|
|
4848
|
+
"- If write_file/edit_file fails due to malformed arguments, regenerate complete JSON.\n"
|
|
4849
|
+
"- If output looks truncated, split into smaller subtasks.\n"
|
|
4850
|
+
"- For shell commands: use bash_command for execution, not file tools.\n"
|
|
4851
|
+
"- Always end thinking sections with either a final answer or one tool call.\n"
|
|
4852
|
+
"- Never stop at thinking-only content without an action.\n"
|
|
4853
|
+
),
|
|
4854
|
+
},
|
|
4855
|
+
"context-management": {
|
|
4856
|
+
"description": "Context compaction triggers, context_recall usage, step sizing",
|
|
4857
|
+
"body": (
|
|
4858
|
+
"# Context Management Guide\n"
|
|
4859
|
+
"- Context has a token upper bound; keep steps compact.\n"
|
|
4860
|
+
"- When <compact-resume> hint appears, inherit pending todos and continue immediately.\n"
|
|
4861
|
+
"- After compaction, use context_recall to fetch archived messages by segment_id/query.\n"
|
|
4862
|
+
"- Do not guess content that was compacted away—recall it first.\n"
|
|
4863
|
+
"- For large tasks, break into subtasks to avoid context overflow.\n"
|
|
4864
|
+
),
|
|
4865
|
+
},
|
|
4866
|
+
"multi-agent-guide": {
|
|
4867
|
+
"description": "Blackboard read/write norms, ask_colleague usage, handoff format",
|
|
4868
|
+
"body": (
|
|
4869
|
+
"# Multi-Agent Guide\n"
|
|
4870
|
+
"- Use read_from_blackboard to check shared state before acting.\n"
|
|
4871
|
+
"- Use write_to_blackboard to record progress, artifacts, and blockers.\n"
|
|
4872
|
+
"- Use ask_colleague with structured intent for inter-agent communication:\n"
|
|
4873
|
+
" - explorer->developer: 'handoff' with research findings\n"
|
|
4874
|
+
" - developer->reviewer: 'review_request' with changed files list\n"
|
|
4875
|
+
" - reviewer->developer: 'fix_request' with concrete failure evidence\n"
|
|
4876
|
+
"- Handoffs should include enough context for the target agent to act independently.\n"
|
|
4877
|
+
"- Keep blackboard updates atomic and concise.\n"
|
|
4878
|
+
),
|
|
4879
|
+
},
|
|
4880
|
+
"code-review-checklist": {
|
|
4881
|
+
"description": "Reviewer role checklist for code verification",
|
|
4882
|
+
"body": (
|
|
4883
|
+
"# Code Review Checklist\n"
|
|
4884
|
+
"1. Does the implementation match the original goal?\n"
|
|
4885
|
+
"2. Are there syntax errors? Run linting/type checks if available.\n"
|
|
4886
|
+
"3. Are there obvious logic bugs or edge cases missed?\n"
|
|
4887
|
+
"4. Do tests pass? If no tests exist, verify manually.\n"
|
|
4888
|
+
"5. Are there security issues (injection, XSS, hardcoded secrets)?\n"
|
|
4889
|
+
"6. Write review_feedback to blackboard with pass/fail and evidence.\n"
|
|
4890
|
+
"7. If fail: send fix_request via ask_colleague with specific issues.\n"
|
|
4891
|
+
),
|
|
4892
|
+
},
|
|
4893
|
+
"finish-protocol": {
|
|
4894
|
+
"description": "When and how to call finish_current_task correctly",
|
|
4895
|
+
"body": (
|
|
4896
|
+
"# Finish Protocol\n"
|
|
4897
|
+
"- Call finish_current_task when all required work is complete.\n"
|
|
4898
|
+
"- Include a concise summary of what was done.\n"
|
|
4899
|
+
"- For multi-agent mode: finish triggers auto-summary from blackboard state.\n"
|
|
4900
|
+
"- Do not finish if there are known failing tests or unresolved blockers.\n"
|
|
4901
|
+
"- If todos have stale pending items but work is done, finish anyway—stale items are cleared automatically.\n"
|
|
4902
|
+
"- Summary format: list modified files, key changes, and validation status.\n"
|
|
4903
|
+
),
|
|
4904
|
+
},
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4674
4907
|
class SkillStore:
|
|
4675
4908
|
def __init__(self, skills_root: Path):
|
|
4676
4909
|
self.skills_root = skills_root
|
|
@@ -5075,9 +5308,34 @@ class SkillStore:
|
|
|
5075
5308
|
self._load_local_skills()
|
|
5076
5309
|
self._load_manifest_providers()
|
|
5077
5310
|
self._load_clawhub_autodetect()
|
|
5311
|
+
self._inject_builtin_skills()
|
|
5078
5312
|
self.fingerprint = fp
|
|
5079
5313
|
self.last_reload_ts = now
|
|
5080
5314
|
|
|
5315
|
+
def _inject_builtin_skills(self):
|
|
5316
|
+
"""Register built-in skill guides for small-model support."""
|
|
5317
|
+
provider_id = "builtin"
|
|
5318
|
+
if not self._provider_exists(provider_id):
|
|
5319
|
+
self._register_provider(
|
|
5320
|
+
provider_id, "builtin", "1.0",
|
|
5321
|
+
"Built-in skill guides for agent operation",
|
|
5322
|
+
)
|
|
5323
|
+
for skill_name, data in _BUILTIN_SKILLS.items():
|
|
5324
|
+
key = f"{provider_id}:{skill_name}"
|
|
5325
|
+
if key in self.skills:
|
|
5326
|
+
continue
|
|
5327
|
+
self._register_skill(
|
|
5328
|
+
provider_id=provider_id,
|
|
5329
|
+
protocol="builtin",
|
|
5330
|
+
protocol_version="1.0",
|
|
5331
|
+
name=skill_name,
|
|
5332
|
+
description=data["description"],
|
|
5333
|
+
meta={"builtin": True},
|
|
5334
|
+
body=data["body"],
|
|
5335
|
+
skill_path="(builtin)",
|
|
5336
|
+
attachments=[],
|
|
5337
|
+
)
|
|
5338
|
+
|
|
5081
5339
|
def descriptions(self, max_items: int = SKILL_PROMPT_MAX_ITEMS, max_chars: int = SKILL_PROMPT_MAX_CHARS) -> str:
|
|
5082
5340
|
if not self.skills:
|
|
5083
5341
|
return "(no skills)"
|
|
@@ -6486,11 +6744,14 @@ class OllamaClient:
|
|
|
6486
6744
|
think: bool = False,
|
|
6487
6745
|
on_thinking_chunk=None,
|
|
6488
6746
|
) -> dict:
|
|
6747
|
+
effective_max = max_tokens
|
|
6748
|
+
if tools:
|
|
6749
|
+
effective_max = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
|
|
6489
6750
|
payload = {
|
|
6490
6751
|
"model": self.model,
|
|
6491
6752
|
"messages": req_messages,
|
|
6492
6753
|
"stream": True,
|
|
6493
|
-
"options": {"temperature": temperature, "num_predict":
|
|
6754
|
+
"options": {"temperature": temperature, "num_predict": effective_max},
|
|
6494
6755
|
}
|
|
6495
6756
|
if tools:
|
|
6496
6757
|
payload["tools"] = tools
|
|
@@ -6503,6 +6764,7 @@ class OllamaClient:
|
|
|
6503
6764
|
full_content: list[str] = []
|
|
6504
6765
|
full_thinking: list[str] = []
|
|
6505
6766
|
tool_calls: list[dict] = []
|
|
6767
|
+
done_reason = ""
|
|
6506
6768
|
try:
|
|
6507
6769
|
with urlopen(req, timeout=self.timeout) as resp:
|
|
6508
6770
|
while True:
|
|
@@ -6542,6 +6804,7 @@ class OllamaClient:
|
|
|
6542
6804
|
if tcs:
|
|
6543
6805
|
tool_calls = self._normalize_tool_calls(tcs)
|
|
6544
6806
|
if part.get("done"):
|
|
6807
|
+
done_reason = str(part.get("done_reason") or "").strip().lower()
|
|
6545
6808
|
break
|
|
6546
6809
|
except HTTPError as exc:
|
|
6547
6810
|
text = exc.read().decode("utf-8", errors="replace")
|
|
@@ -6554,7 +6817,7 @@ class OllamaClient:
|
|
|
6554
6817
|
"content": content,
|
|
6555
6818
|
"thinking": thinking_content,
|
|
6556
6819
|
"tool_calls": tool_calls,
|
|
6557
|
-
"raw": {"streamed": True},
|
|
6820
|
+
"raw": {"streamed": True, "done_reason": done_reason},
|
|
6558
6821
|
}
|
|
6559
6822
|
|
|
6560
6823
|
def chat(
|
|
@@ -6618,11 +6881,14 @@ class OllamaClient:
|
|
|
6618
6881
|
fallback_status = {0, 400, 404, 405, 500, 501, 502, 503, 504}
|
|
6619
6882
|
if status not in fallback_status:
|
|
6620
6883
|
raise
|
|
6884
|
+
effective_max_native = max_tokens
|
|
6885
|
+
if tools:
|
|
6886
|
+
effective_max_native = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
|
|
6621
6887
|
native_payload = {
|
|
6622
6888
|
"model": self.model,
|
|
6623
6889
|
"messages": req_messages,
|
|
6624
6890
|
"stream": False,
|
|
6625
|
-
"options": {"temperature": temperature, "num_predict":
|
|
6891
|
+
"options": {"temperature": temperature, "num_predict": effective_max_native},
|
|
6626
6892
|
}
|
|
6627
6893
|
if tools:
|
|
6628
6894
|
native_payload["tools"] = tools
|
|
@@ -6918,6 +7184,7 @@ class SessionState:
|
|
|
6918
7184
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
6919
7185
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
6920
7186
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
7187
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
6921
7188
|
ui_language: str = DEFAULT_UI_LANGUAGE,
|
|
6922
7189
|
js_lib_root: Path | None = None,
|
|
6923
7190
|
owner_user_id: str = "",
|
|
@@ -6964,6 +7231,7 @@ class SessionState:
|
|
|
6964
7231
|
self.multimodal_capability_cache: dict[str, dict] = {}
|
|
6965
7232
|
self.failed_selections: list[str] = []
|
|
6966
7233
|
self.todo = TodoManager()
|
|
7234
|
+
self.single_advance_prompt_enhance = False
|
|
6967
7235
|
self.skills = SkillStore(skills_root)
|
|
6968
7236
|
self.skill_load_cache: dict[str, dict] = {}
|
|
6969
7237
|
self.skills_last_refresh_ts = 0.0
|
|
@@ -6973,8 +7241,9 @@ class SessionState:
|
|
|
6973
7241
|
self.bus = MessageBus(self.root / "team" / "inbox", crypto)
|
|
6974
7242
|
self.worktrees = WorktreeManager(self.id, self.tasks, self.root, crypto, repo_root)
|
|
6975
7243
|
self.messages: list[dict] = []
|
|
6976
|
-
self.
|
|
6977
|
-
self.
|
|
7244
|
+
self.agent_messages: list[dict] = [] # unified agent context (replaces contexts + manager_context)
|
|
7245
|
+
self.contexts: dict[str, list[dict]] = {role: [] for role in AGENT_ROLES} # kept as view caches
|
|
7246
|
+
self.manager_context: list[dict] = [] # kept as view cache
|
|
6978
7247
|
self.blackboard: dict = {}
|
|
6979
7248
|
self.agent_bus_messages: list[dict] = []
|
|
6980
7249
|
self.manager_routes: list[dict] = []
|
|
@@ -7055,6 +7324,7 @@ class SessionState:
|
|
|
7055
7324
|
MIN_AGENT_ROUNDS,
|
|
7056
7325
|
min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
|
|
7057
7326
|
)
|
|
7327
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
7058
7328
|
self.max_run_seconds = normalize_timeout_seconds(
|
|
7059
7329
|
max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
|
|
7060
7330
|
minimum=MIN_RUN_TIMEOUT_SECONDS,
|
|
@@ -8037,6 +8307,13 @@ class SessionState:
|
|
|
8037
8307
|
if isinstance(row, dict):
|
|
8038
8308
|
clean_manager_context.append(dict(row))
|
|
8039
8309
|
self.manager_context = clean_manager_context[-400:]
|
|
8310
|
+
raw_agent_messages = raw.get("agent_messages", [])
|
|
8311
|
+
if isinstance(raw_agent_messages, list):
|
|
8312
|
+
clean_am: list[dict] = []
|
|
8313
|
+
for row in raw_agent_messages[-1200:]:
|
|
8314
|
+
if isinstance(row, dict):
|
|
8315
|
+
clean_am.append(dict(row))
|
|
8316
|
+
self.agent_messages = clean_am[-800:]
|
|
8040
8317
|
raw_blackboard = raw.get("blackboard", {})
|
|
8041
8318
|
self.blackboard = self._normalize_blackboard(raw_blackboard)
|
|
8042
8319
|
raw_bus = raw.get("agent_bus_messages", [])
|
|
@@ -8159,6 +8436,7 @@ class SessionState:
|
|
|
8159
8436
|
"active_agent_role": str(self.active_agent_role or ""),
|
|
8160
8437
|
"contexts": {role: list(self.contexts.get(role, []))[-400:] for role in AGENT_ROLES},
|
|
8161
8438
|
"manager_context": list(self.manager_context)[-400:],
|
|
8439
|
+
"agent_messages": list(self.agent_messages)[-800:],
|
|
8162
8440
|
"blackboard": self._normalize_blackboard(self.blackboard),
|
|
8163
8441
|
"agent_bus_messages": list(self.agent_bus_messages)[-240:],
|
|
8164
8442
|
"manager_routes": list(self.manager_routes)[-240:],
|
|
@@ -8625,75 +8903,36 @@ class SessionState:
|
|
|
8625
8903
|
research_hint = self._deep_research_boost_instruction()
|
|
8626
8904
|
runtime_level = int(self.runtime_task_level or 0)
|
|
8627
8905
|
runtime_mode = self._effective_execution_mode()
|
|
8628
|
-
|
|
8629
|
-
runtime_participants = [
|
|
8630
|
-
role for role in [self._sanitize_agent_role(x) for x in self.runtime_participants] if role
|
|
8631
|
-
][:3]
|
|
8632
|
-
if runtime_level in {1, 2} or runtime_mode == EXECUTION_MODE_SINGLE:
|
|
8633
|
-
todo_hint = (
|
|
8634
|
-
"Current run is direct single-agent mode. "
|
|
8635
|
-
"Do not create Todo/task scaffolding unless user explicitly asks for project management. "
|
|
8636
|
-
"Prioritize direct final response or one concrete minimal tool action."
|
|
8637
|
-
)
|
|
8638
|
-
else:
|
|
8639
|
-
todo_hint = (
|
|
8640
|
-
"For non-trivial tasks, call TodoWrite early with 3-7 concise items "
|
|
8641
|
-
"(exactly one in_progress), then update only when plan/status changes. "
|
|
8642
|
-
"Avoid redundant TodoWrite calls. "
|
|
8643
|
-
"If TodoWrite fails or repeats with no changes, call TodoWriteRescue with simple string items."
|
|
8644
|
-
)
|
|
8645
|
-
route_hint = (
|
|
8646
|
-
f"Runtime manager classification: level={runtime_level or '-'}, mode={runtime_mode}, "
|
|
8647
|
-
f"scale_preference={self.runtime_scale_preference or 'balanced'}, "
|
|
8648
|
-
f"assigned_expert={runtime_assigned}, participants={','.join(runtime_participants) or runtime_assigned}. "
|
|
8649
|
-
)
|
|
8650
|
-
if int(self.runtime_round_budget or 0) <= 0:
|
|
8651
|
-
budget_hint = (
|
|
8652
|
-
"Runtime budget policy: unlimited tier budget; keep each step compact and action-oriented; "
|
|
8653
|
-
"never stall on long planning text."
|
|
8654
|
-
)
|
|
8655
|
-
else:
|
|
8656
|
-
budget_hint = (
|
|
8657
|
-
f"Runtime budget policy: internal cadence budget={int(self.runtime_round_budget)} "
|
|
8658
|
-
"for compact reasoning and fast handoffs. "
|
|
8659
|
-
"Budget controls thought depth only and must not be used as an early-stop user-facing reason."
|
|
8660
|
-
)
|
|
8906
|
+
budget = int(self.runtime_round_budget or 0)
|
|
8661
8907
|
html_block = f"{html_hint}\n\n" if html_hint else ""
|
|
8662
8908
|
research_block = f"{research_hint}\n\n" if research_hint else ""
|
|
8909
|
+
_is_single_no_enhance = (
|
|
8910
|
+
runtime_mode == EXECUTION_MODE_SINGLE
|
|
8911
|
+
and not self.single_advance_prompt_enhance
|
|
8912
|
+
)
|
|
8913
|
+
skill_hint = (
|
|
8914
|
+
"Use load_skill for workspace-paths and tool-best-practices if needed. "
|
|
8915
|
+
if _is_single_no_enhance
|
|
8916
|
+
else (
|
|
8917
|
+
"Use load_skill for guidance on specific topics "
|
|
8918
|
+
"(workspace-paths, task-management, tool-best-practices, context-management). "
|
|
8919
|
+
"If execution stalls, load_skill('execution-degradation-recovery'). "
|
|
8920
|
+
)
|
|
8921
|
+
)
|
|
8663
8922
|
return (
|
|
8664
|
-
f"You are a coding agent
|
|
8665
|
-
f"
|
|
8666
|
-
"
|
|
8667
|
-
"
|
|
8923
|
+
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
8924
|
+
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
8925
|
+
f"budget={'unlimited' if budget <= 0 else budget}. "
|
|
8926
|
+
f"Context limit ~{self.context_token_upper_bound} tokens. "
|
|
8668
8927
|
f"{_detect_os_shell_instruction()} "
|
|
8669
|
-
"Use tools to inspect
|
|
8670
|
-
|
|
8671
|
-
f"{
|
|
8672
|
-
f"{todo_hint} "
|
|
8673
|
-
"When all required work is complete, call finish_current_task (or finish_task / mark_done) "
|
|
8674
|
-
"to end execution cleanly "
|
|
8675
|
-
"(especially when todos may still contain stale pending items). "
|
|
8676
|
-
"If you output internal reasoning/thinking, always end that section and then output either "
|
|
8677
|
-
"a final answer or one concrete tool call; never stop at thinking-only content. "
|
|
8678
|
-
"For multi-step work use task_create/task_update/task_list as needed. "
|
|
8679
|
-
"Use load_skill only when needed; use provider:name if skill name is ambiguous. "
|
|
8680
|
-
"Loaded skill content is cached per session; do not repeatedly call load_skill for the same skill unless needed. "
|
|
8681
|
-
"If execution stalls (no tool calls / repeated failures), load_skill('execution-degradation-recovery') and follow it. "
|
|
8682
|
-
"Use list_skill_providers and list_skill_protocols to inspect dynamic backend skill integrations. "
|
|
8683
|
-
"If user asks to save generated guidance/workflow as reusable skill, call write_skill to write SKILL.md under global ./skills. "
|
|
8684
|
-
"When changing files, prefer write_file/edit_file so the UI can render line-level diffs. "
|
|
8685
|
-
"If a write_file/edit_file tool call fails due malformed or truncated arguments, regenerate and resend the complete JSON arguments. "
|
|
8686
|
-
"If output or tool arguments look truncated, split work into smaller subtasks and execute one subtask at a time. "
|
|
8687
|
-
"If context has been compacted, call context_recall to fetch exact archived messages by segment_id/query before guessing. "
|
|
8688
|
-
"When a <compact-resume> hint appears, inherit pending todos/tasks and continue exploration immediately. "
|
|
8689
|
-
f"Current context upper bound is ~{self.context_token_upper_bound} tokens; keep steps compact to stay under this limit. "
|
|
8690
|
-
"When user asks to modify uploaded content, prioritize files under the uploaded workspace paths.\n\n"
|
|
8691
|
-
"If user asks to generate image/audio/video, use generate_media when active model capability supports it.\n\n"
|
|
8928
|
+
"Use tools to inspect, edit, and execute. "
|
|
8929
|
+
"Call finish_current_task when done. "
|
|
8930
|
+
f"{skill_hint}"
|
|
8692
8931
|
f"{html_block}"
|
|
8693
8932
|
f"{research_block}"
|
|
8694
8933
|
f"{model_language_instruction(self.ui_language)}\n\n"
|
|
8695
|
-
f"
|
|
8696
|
-
f"
|
|
8934
|
+
f"Uploads:\n{uploads_ctx}\n\n"
|
|
8935
|
+
f"Skills:\n{self.skills.descriptions()}"
|
|
8697
8936
|
)
|
|
8698
8937
|
|
|
8699
8938
|
def _estimate_tokens(self) -> int:
|
|
@@ -9261,7 +9500,7 @@ class SessionState:
|
|
|
9261
9500
|
if tool_calls:
|
|
9262
9501
|
return False
|
|
9263
9502
|
|
|
9264
|
-
near_limit = output_tokens >= int(
|
|
9503
|
+
near_limit = output_tokens >= int(self.max_output_tokens * 0.90)
|
|
9265
9504
|
# Mid-size outputs (e.g. planning text ending with a Chinese colon) should not be
|
|
9266
9505
|
# treated as truncation unless close to max tokens or JSON-like unfinished payload.
|
|
9267
9506
|
json_like_tail = bool(
|
|
@@ -9406,17 +9645,108 @@ class SessionState:
|
|
|
9406
9645
|
task_ids = self._create_truncation_subtasks(reason)
|
|
9407
9646
|
self._inject_truncation_rescue_hint(reason, output_tokens, task_ids)
|
|
9408
9647
|
|
|
9409
|
-
def
|
|
9648
|
+
def _find_tool_name_by_id(self, messages: list[dict], tool_use_id: str) -> str:
|
|
9649
|
+
"""Find tool name from preceding assistant message by tool_use_id."""
|
|
9650
|
+
if not tool_use_id:
|
|
9651
|
+
return ""
|
|
9652
|
+
for msg in reversed(messages):
|
|
9653
|
+
if msg.get("role") != "assistant":
|
|
9654
|
+
continue
|
|
9655
|
+
for tc in msg.get("tool_calls", []):
|
|
9656
|
+
if isinstance(tc, dict) and tc.get("id") == tool_use_id:
|
|
9657
|
+
fn = tc.get("function", {})
|
|
9658
|
+
return str(fn.get("name", "")) if isinstance(fn, dict) else ""
|
|
9659
|
+
content = msg.get("content")
|
|
9660
|
+
if isinstance(content, list):
|
|
9661
|
+
for block in content:
|
|
9662
|
+
if isinstance(block, dict) and block.get("type") == "tool_use" and block.get("id") == tool_use_id:
|
|
9663
|
+
return str(block.get("name", ""))
|
|
9664
|
+
return ""
|
|
9665
|
+
|
|
9666
|
+
def _microcompact(self, keep_recent: int = 3):
|
|
9667
|
+
"""Replace old tool results with compact placeholders to save tokens.
|
|
9668
|
+
|
|
9669
|
+
Handles both OpenAI-style (role=tool) and Anthropic-style (tool_result blocks)
|
|
9670
|
+
messages. Keeps the most recent `keep_recent` tool interactions intact.
|
|
9671
|
+
"""
|
|
9672
|
+
# Phase 1: OpenAI-style role=tool messages
|
|
9410
9673
|
tool_messages = [m for m in self.messages if m.get("role") == "tool"]
|
|
9411
|
-
if len(tool_messages)
|
|
9674
|
+
if len(tool_messages) > keep_recent:
|
|
9675
|
+
kept = tool_messages[-keep_recent:]
|
|
9676
|
+
keep_ids = {id(x) for x in kept}
|
|
9677
|
+
for msg in self.messages:
|
|
9678
|
+
if msg.get("role") == "tool" and id(msg) not in keep_ids:
|
|
9679
|
+
content = msg.get("content", "")
|
|
9680
|
+
if isinstance(content, str) and len(content) > 120:
|
|
9681
|
+
msg["content"] = "[cleared by microcompact]"
|
|
9682
|
+
|
|
9683
|
+
# Phase 2: Anthropic-style tool_result blocks in user messages
|
|
9684
|
+
assistant_indices = [i for i, m in enumerate(self.messages) if m.get("role") == "assistant"]
|
|
9685
|
+
if len(assistant_indices) <= keep_recent:
|
|
9686
|
+
return
|
|
9687
|
+
cutoff = assistant_indices[-keep_recent]
|
|
9688
|
+
for i in range(cutoff):
|
|
9689
|
+
msg = self.messages[i]
|
|
9690
|
+
if msg.get("role") == "user" and isinstance(msg.get("content"), list):
|
|
9691
|
+
changed = False
|
|
9692
|
+
new_content = []
|
|
9693
|
+
for block in msg["content"]:
|
|
9694
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
9695
|
+
block_content = block.get("content", "")
|
|
9696
|
+
if isinstance(block_content, str) and len(block_content) > 120:
|
|
9697
|
+
tool_id = block.get("tool_use_id", "")
|
|
9698
|
+
tool_name = self._find_tool_name_by_id(self.messages[:i], tool_id) or "tool"
|
|
9699
|
+
new_content.append({
|
|
9700
|
+
"type": "tool_result",
|
|
9701
|
+
"tool_use_id": tool_id,
|
|
9702
|
+
"content": f"[Previous: used {tool_name}]",
|
|
9703
|
+
})
|
|
9704
|
+
changed = True
|
|
9705
|
+
else:
|
|
9706
|
+
new_content.append(block)
|
|
9707
|
+
else:
|
|
9708
|
+
new_content.append(block)
|
|
9709
|
+
if changed:
|
|
9710
|
+
msg["content"] = new_content
|
|
9711
|
+
|
|
9712
|
+
def _microcompact_agent_messages(self, messages: list[dict], keep_recent: int = 3):
|
|
9713
|
+
"""Apply microcompact to an agent-specific message list (e.g. agent_messages filtered by role)."""
|
|
9714
|
+
tool_messages = [m for m in messages if m.get("role") == "tool"]
|
|
9715
|
+
if len(tool_messages) > keep_recent:
|
|
9716
|
+
kept = tool_messages[-keep_recent:]
|
|
9717
|
+
keep_ids = {id(x) for x in kept}
|
|
9718
|
+
for msg in messages:
|
|
9719
|
+
if msg.get("role") == "tool" and id(msg) not in keep_ids:
|
|
9720
|
+
content = msg.get("content", "")
|
|
9721
|
+
if isinstance(content, str) and len(content) > 120:
|
|
9722
|
+
msg["content"] = "[cleared by microcompact]"
|
|
9723
|
+
assistant_indices = [i for i, m in enumerate(messages) if m.get("role") == "assistant"]
|
|
9724
|
+
if len(assistant_indices) <= keep_recent:
|
|
9412
9725
|
return
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
if msg.get("role") == "
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9726
|
+
cutoff = assistant_indices[-keep_recent]
|
|
9727
|
+
for i in range(cutoff):
|
|
9728
|
+
msg = messages[i]
|
|
9729
|
+
if msg.get("role") == "user" and isinstance(msg.get("content"), list):
|
|
9730
|
+
changed = False
|
|
9731
|
+
new_content = []
|
|
9732
|
+
for block in msg["content"]:
|
|
9733
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
9734
|
+
block_content = block.get("content", "")
|
|
9735
|
+
if isinstance(block_content, str) and len(block_content) > 120:
|
|
9736
|
+
tool_id = block.get("tool_use_id", "")
|
|
9737
|
+
tool_name = self._find_tool_name_by_id(messages[:i], tool_id) or "tool"
|
|
9738
|
+
new_content.append({
|
|
9739
|
+
"type": "tool_result",
|
|
9740
|
+
"tool_use_id": tool_id,
|
|
9741
|
+
"content": f"[Previous: used {tool_name}]",
|
|
9742
|
+
})
|
|
9743
|
+
changed = True
|
|
9744
|
+
else:
|
|
9745
|
+
new_content.append(block)
|
|
9746
|
+
else:
|
|
9747
|
+
new_content.append(block)
|
|
9748
|
+
if changed:
|
|
9749
|
+
msg["content"] = new_content
|
|
9420
9750
|
|
|
9421
9751
|
def _estimate_messages_tokens(self, rows: list[dict]) -> int:
|
|
9422
9752
|
try:
|
|
@@ -10090,6 +10420,17 @@ class SessionState:
|
|
|
10090
10420
|
return data.decode("latin-1", errors="ignore")
|
|
10091
10421
|
|
|
10092
10422
|
def _extract_pdf_text(self, pdf_path: Path) -> str:
|
|
10423
|
+
# 优先使用 pdfminer.six(纯 Python,无外部依赖)
|
|
10424
|
+
try:
|
|
10425
|
+
from pdfminer.high_level import extract_text
|
|
10426
|
+
text = extract_text(str(pdf_path))
|
|
10427
|
+
if text and text.strip():
|
|
10428
|
+
return text.strip()
|
|
10429
|
+
except ImportError:
|
|
10430
|
+
pass
|
|
10431
|
+
except Exception:
|
|
10432
|
+
pass
|
|
10433
|
+
# 降级:pdftotext CLI
|
|
10093
10434
|
tool = shutil.which("pdftotext")
|
|
10094
10435
|
if tool:
|
|
10095
10436
|
try:
|
|
@@ -10103,6 +10444,7 @@ class SessionState:
|
|
|
10103
10444
|
return r.stdout.strip()
|
|
10104
10445
|
except Exception:
|
|
10105
10446
|
pass
|
|
10447
|
+
# 最终降级:regex 提取
|
|
10106
10448
|
try:
|
|
10107
10449
|
raw = pdf_path.read_bytes()
|
|
10108
10450
|
text = raw.decode("latin-1", errors="ignore")
|
|
@@ -10435,6 +10777,16 @@ class SessionState:
|
|
|
10435
10777
|
lines.append(chunk)
|
|
10436
10778
|
lines.append("</uploaded_excerpt>")
|
|
10437
10779
|
remaining -= len(chunk)
|
|
10780
|
+
# 提示模型可直接读取 .parsed.md 文件获取完整解析文本
|
|
10781
|
+
item_kind = item.get("kind", "file")
|
|
10782
|
+
if item_kind not in ("text", "code"):
|
|
10783
|
+
wp = item.get("workspace_path", "")
|
|
10784
|
+
if wp:
|
|
10785
|
+
from pathlib import PurePosixPath
|
|
10786
|
+
stem = PurePosixPath(wp).stem
|
|
10787
|
+
parent = str(PurePosixPath(wp).parent)
|
|
10788
|
+
parsed_rel = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
|
|
10789
|
+
lines.append(f" (parsed text available at: {parsed_rel} — use read_file to access full content)")
|
|
10438
10790
|
return "\n".join(lines)
|
|
10439
10791
|
|
|
10440
10792
|
def add_upload(self, filename: str, raw: bytes, mime: str = "") -> dict:
|
|
@@ -10445,10 +10797,42 @@ class SessionState:
|
|
|
10445
10797
|
stored.write_bytes(raw)
|
|
10446
10798
|
ext = stored.suffix.lower()
|
|
10447
10799
|
text_like_ext = {
|
|
10448
|
-
".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".h", ".hpp",
|
|
10449
|
-
".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".sql", ".html",
|
|
10450
|
-
".css", ".
|
|
10451
|
-
".
|
|
10800
|
+
".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl",
|
|
10801
|
+
".go", ".rs", ".rb", ".php", ".swift", ".kt", ".kts", ".scala", ".sh", ".bash", ".zsh", ".fish", ".sql", ".html", ".htm",
|
|
10802
|
+
".css", ".sass", ".scss", ".less", ".styl", ".json", ".jsonc", ".yaml", ".yml", ".xml", ".xsd", ".xsl",
|
|
10803
|
+
".toml", ".ini", ".cfg", ".conf", ".env", ".properties", ".md", ".mdx", ".txt", ".rst", ".log",
|
|
10804
|
+
".ipynb", ".vue", ".svelte", ".cs", ".m", ".mm", ".r", ".pl", ".pm", ".csv", ".tsv",
|
|
10805
|
+
# Fortran
|
|
10806
|
+
".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
|
|
10807
|
+
# 更多语言
|
|
10808
|
+
".zig", ".nim", ".v", ".d", ".ada", ".adb", ".ads",
|
|
10809
|
+
".asm", ".s",
|
|
10810
|
+
".bas", ".vb", ".vbs", ".vba",
|
|
10811
|
+
".bat", ".cmd", ".ps1", ".psm1",
|
|
10812
|
+
".clj", ".cljs", ".cljc", ".edn",
|
|
10813
|
+
".coffee", ".cr", ".dart",
|
|
10814
|
+
".dockerfile",
|
|
10815
|
+
".erl", ".hrl", ".ex", ".exs",
|
|
10816
|
+
".fs", ".fsi", ".fsx",
|
|
10817
|
+
".gradle", ".groovy", ".gvy",
|
|
10818
|
+
".hs", ".lhs",
|
|
10819
|
+
".jl", ".lua",
|
|
10820
|
+
".lisp", ".cl", ".el", ".scm", ".rkt",
|
|
10821
|
+
".mk", ".cmake",
|
|
10822
|
+
".ml", ".mli", ".nix",
|
|
10823
|
+
".pas", ".pp", ".inc",
|
|
10824
|
+
".pde", ".ino",
|
|
10825
|
+
".proto", ".purs",
|
|
10826
|
+
".raku", ".p6",
|
|
10827
|
+
".sol",
|
|
10828
|
+
".sv", ".svh", ".vh", ".vhd", ".vhdl",
|
|
10829
|
+
".tcl", ".tk",
|
|
10830
|
+
".tf", ".tfvars", ".hcl",
|
|
10831
|
+
".tex", ".bib", ".sty", ".cls", ".typ",
|
|
10832
|
+
".wat",
|
|
10833
|
+
".diff", ".patch",
|
|
10834
|
+
".graphql", ".gql",
|
|
10835
|
+
".prisma", ".svg",
|
|
10452
10836
|
}
|
|
10453
10837
|
parsed_excerpt = ""
|
|
10454
10838
|
kind = "binary"
|
|
@@ -10489,6 +10873,15 @@ class SessionState:
|
|
|
10489
10873
|
workspace_target = self._upload_workspace_target(safe_name)
|
|
10490
10874
|
workspace_target.parent.mkdir(parents=True, exist_ok=True)
|
|
10491
10875
|
workspace_target.write_bytes(raw)
|
|
10876
|
+
# 当 parsed_excerpt 非空且原始文件不是纯文本时,保存解析结果为 .parsed.md
|
|
10877
|
+
parsed_target: Path | None = None
|
|
10878
|
+
if parsed_excerpt and kind not in ("text", "code"):
|
|
10879
|
+
parsed_target = workspace_target.parent / f"{workspace_target.stem}.parsed.md"
|
|
10880
|
+
try:
|
|
10881
|
+
header = f"# {safe_name}\n\n> Auto-parsed from uploaded {kind} file ({len(raw)} bytes)\n\n"
|
|
10882
|
+
parsed_target.write_text(header + parsed_excerpt, encoding="utf-8")
|
|
10883
|
+
except Exception:
|
|
10884
|
+
parsed_target = None # 解析文件保存失败不影响主流程
|
|
10492
10885
|
workspace_rel = self._session_rel(workspace_target)
|
|
10493
10886
|
meta = {
|
|
10494
10887
|
"id": upload_id,
|
|
@@ -10507,6 +10900,9 @@ class SessionState:
|
|
|
10507
10900
|
self.uploads = self.uploads[-80:]
|
|
10508
10901
|
self.updated_at = now_ts()
|
|
10509
10902
|
self._persist()
|
|
10903
|
+
if parsed_excerpt:
|
|
10904
|
+
bb_content = f"[upload:{safe_name}]\n{trim(parsed_excerpt, BLACKBOARD_MAX_TEXT - 200)}"
|
|
10905
|
+
self._blackboard_append_section("research_notes", "system", bb_content)
|
|
10510
10906
|
self._emit(
|
|
10511
10907
|
"upload",
|
|
10512
10908
|
{
|
|
@@ -10514,7 +10910,10 @@ class SessionState:
|
|
|
10514
10910
|
"workspace_path": workspace_rel,
|
|
10515
10911
|
"kind": kind,
|
|
10516
10912
|
"size": len(raw),
|
|
10517
|
-
"summary":
|
|
10913
|
+
"summary": (
|
|
10914
|
+
f"upload: {safe_name} -> {workspace_rel}"
|
|
10915
|
+
+ (f" (parsed text: {self._session_rel(parsed_target)})" if parsed_target else "")
|
|
10916
|
+
),
|
|
10518
10917
|
"preview": trim(parsed_excerpt, 500),
|
|
10519
10918
|
},
|
|
10520
10919
|
)
|
|
@@ -10611,6 +11010,12 @@ class SessionState:
|
|
|
10611
11010
|
"completed",
|
|
10612
11011
|
"finished",
|
|
10613
11012
|
"all set",
|
|
11013
|
+
# 明确表示拒绝/无法完成也应视为终结
|
|
11014
|
+
"抱歉",
|
|
11015
|
+
"sorry",
|
|
11016
|
+
"无法",
|
|
11017
|
+
"cannot",
|
|
11018
|
+
"unable",
|
|
10614
11019
|
]
|
|
10615
11020
|
if any(x in t for x in done_markers):
|
|
10616
11021
|
return False
|
|
@@ -10692,6 +11097,17 @@ class SessionState:
|
|
|
10692
11097
|
"that's all",
|
|
10693
11098
|
"that is all",
|
|
10694
11099
|
"as requested",
|
|
11100
|
+
# 明确表示无法完成的标记
|
|
11101
|
+
"抱歉,我无法",
|
|
11102
|
+
"无法直接获取",
|
|
11103
|
+
"无法完成",
|
|
11104
|
+
"cannot be done",
|
|
11105
|
+
"unable to",
|
|
11106
|
+
"not possible",
|
|
11107
|
+
"建议您通过",
|
|
11108
|
+
"建议你通过",
|
|
11109
|
+
"i cannot",
|
|
11110
|
+
"i'm unable",
|
|
10695
11111
|
]
|
|
10696
11112
|
return any(x in t for x in done_markers)
|
|
10697
11113
|
|
|
@@ -11098,7 +11514,7 @@ class SessionState:
|
|
|
11098
11514
|
return False
|
|
11099
11515
|
if not str(thinking_text or "").strip():
|
|
11100
11516
|
return False
|
|
11101
|
-
threshold = int(max(1,
|
|
11517
|
+
threshold = int(max(1, self.max_output_tokens) * float(THINKING_BUDGET_FORCE_RATIO))
|
|
11102
11518
|
return int(output_tokens or 0) >= max(1, threshold)
|
|
11103
11519
|
|
|
11104
11520
|
def _is_thinking_only_dead_turn(self, text: str, thinking_text: str, tool_calls: list | None = None) -> bool:
|
|
@@ -12229,12 +12645,48 @@ class SessionState:
|
|
|
12229
12645
|
fp = self._session_path(rel)
|
|
12230
12646
|
content = fp.read_text(encoding="utf-8")
|
|
12231
12647
|
if old_text not in content:
|
|
12232
|
-
|
|
12648
|
+
diag = self._edit_mismatch_diagnostic(content, old_text)
|
|
12649
|
+
return f"Error: text not found in {rel}. {diag}"
|
|
12233
12650
|
fp.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
|
|
12234
12651
|
return f"Edited {rel}"
|
|
12235
12652
|
except Exception as exc:
|
|
12236
12653
|
return f"Error: {type(exc).__name__}: {exc}"
|
|
12237
12654
|
|
|
12655
|
+
def _edit_mismatch_diagnostic(self, content: str, old_text: str) -> str:
|
|
12656
|
+
"""为 edit_file 匹配失败提供诊断信息"""
|
|
12657
|
+
lines = content.splitlines()
|
|
12658
|
+
first_line = old_text.strip().splitlines()[0].strip() if old_text.strip() else ""
|
|
12659
|
+
if not first_line:
|
|
12660
|
+
return "The old_text is empty or whitespace-only."
|
|
12661
|
+
# 搜索 old_text 的第一行在文件中的位置
|
|
12662
|
+
matches = []
|
|
12663
|
+
for i, line in enumerate(lines, 1):
|
|
12664
|
+
if first_line in line:
|
|
12665
|
+
matches.append(i)
|
|
12666
|
+
if matches:
|
|
12667
|
+
loc = ", ".join(str(m) for m in matches[:5])
|
|
12668
|
+
return (
|
|
12669
|
+
f"The first line of old_text was found at line(s) {loc}, "
|
|
12670
|
+
f"but the full multi-line match failed. "
|
|
12671
|
+
f"Likely cause: whitespace or indentation mismatch. "
|
|
12672
|
+
f"Tip: use read_file to get the exact content, then copy it precisely."
|
|
12673
|
+
)
|
|
12674
|
+
# 尝试空白规范化匹配
|
|
12675
|
+
norm_first = " ".join(first_line.split())
|
|
12676
|
+
for i, line in enumerate(lines, 1):
|
|
12677
|
+
norm_line = " ".join(line.split())
|
|
12678
|
+
if norm_first and norm_first in norm_line:
|
|
12679
|
+
return (
|
|
12680
|
+
f"A whitespace-normalized partial match was found near line {i}. "
|
|
12681
|
+
f"The old_text likely has wrong indentation or extra/missing spaces. "
|
|
12682
|
+
f"Use read_file to get exact content."
|
|
12683
|
+
)
|
|
12684
|
+
return (
|
|
12685
|
+
f"No match found. The file has {len(lines)} lines. "
|
|
12686
|
+
f"The old_text first line '{first_line[:60]}' does not appear in the file. "
|
|
12687
|
+
f"The content may have changed since last read. Use read_file to refresh."
|
|
12688
|
+
)
|
|
12689
|
+
|
|
12238
12690
|
def _write_global_skill(self, args: dict) -> str:
|
|
12239
12691
|
rel_raw = str(args.get("path", "") or "").strip().replace("\\", "/")
|
|
12240
12692
|
if not rel_raw:
|
|
@@ -12861,15 +13313,13 @@ class SessionState:
|
|
|
12861
13313
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
12862
13314
|
if bool(self.runtime_goal_reset_pending):
|
|
12863
13315
|
return False, "goal-reset-pending"
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
if
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
if not self._reviewer_final_summary_ready(bb):
|
|
12872
|
-
return False, "reviewer-summary-missing"
|
|
13316
|
+
# Project todo gate: coding tasks must pass compile + test
|
|
13317
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
13318
|
+
task_type = str(profile.get("task_type", "general") or "general")
|
|
13319
|
+
if task_type in ("simple_code", "engineering"):
|
|
13320
|
+
for todo in bb.get("project_todos", []):
|
|
13321
|
+
if todo.get("category") in ("compile_test", "min_test") and todo.get("status") != "completed":
|
|
13322
|
+
return False, f"project-todo-incomplete:{todo.get('category', '')}"
|
|
12873
13323
|
return True, "ok"
|
|
12874
13324
|
|
|
12875
13325
|
def _invalidate_stale_approval_if_needed(
|
|
@@ -13322,9 +13772,6 @@ class SessionState:
|
|
|
13322
13772
|
"decomposer_output": trim(raw_text, 2000),
|
|
13323
13773
|
}
|
|
13324
13774
|
wd = self._normalize_watchdog_state(board.get("watchdog", {}))
|
|
13325
|
-
wd["trigger_count"] = max(0, int(wd.get("trigger_count", 0) or 0)) + 1
|
|
13326
|
-
wd["last_trigger_reason"] = trim(str(reason or "").strip(), 200)
|
|
13327
|
-
wd["last_trigger_ts"] = float(now_ts())
|
|
13328
13775
|
wd["intent_no_tool_streak"] = 0
|
|
13329
13776
|
wd["repeat_no_tool_streak"] = 0
|
|
13330
13777
|
board["watchdog"] = wd
|
|
@@ -13353,6 +13800,39 @@ class SessionState:
|
|
|
13353
13800
|
)
|
|
13354
13801
|
return True
|
|
13355
13802
|
|
|
13803
|
+
def _watchdog_escalate_to_single_developer(self, board: dict, *, reason: str = ""):
|
|
13804
|
+
"""Watchdog 连续 stall 升级:强制降级到 Single+Developer 模式。"""
|
|
13805
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13806
|
+
self.runtime_execution_mode = EXECUTION_MODE_SINGLE
|
|
13807
|
+
self.runtime_participants = ["developer"]
|
|
13808
|
+
self.runtime_assigned_expert = "developer"
|
|
13809
|
+
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
13810
|
+
dq["active"] = False
|
|
13811
|
+
bb["decomposition_queue"] = dq
|
|
13812
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
13813
|
+
profile["execution_mode"] = EXECUTION_MODE_SINGLE
|
|
13814
|
+
profile["participants"] = ["developer"]
|
|
13815
|
+
profile["assigned_expert"] = "developer"
|
|
13816
|
+
bb["task_profile"] = profile
|
|
13817
|
+
self.blackboard = bb
|
|
13818
|
+
self._blackboard_touch()
|
|
13819
|
+
self._blackboard_history(
|
|
13820
|
+
"manager",
|
|
13821
|
+
trim(
|
|
13822
|
+
f"watchdog escalation: forced Single+Developer (trigger_count={int(bb.get('watchdog', {}).get('trigger_count', 0))}, reason={reason})",
|
|
13823
|
+
520,
|
|
13824
|
+
),
|
|
13825
|
+
)
|
|
13826
|
+
self._emit(
|
|
13827
|
+
"status",
|
|
13828
|
+
{
|
|
13829
|
+
"summary": (
|
|
13830
|
+
"watchdog escalation: multi-agent stall detected, "
|
|
13831
|
+
"downgrading to Single+Developer mode"
|
|
13832
|
+
)
|
|
13833
|
+
},
|
|
13834
|
+
)
|
|
13835
|
+
|
|
13356
13836
|
def _watchdog_pick_executor_route(self, board: dict | None = None) -> tuple[dict, dict] | None:
|
|
13357
13837
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13358
13838
|
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
@@ -13542,10 +14022,13 @@ class SessionState:
|
|
|
13542
14022
|
bb = self._ensure_blackboard()
|
|
13543
14023
|
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
13544
14024
|
trigger_reason = ""
|
|
14025
|
+
_is_single = self._effective_execution_mode() == EXECUTION_MODE_SINGLE
|
|
14026
|
+
_intent_th = WATCHDOG_INTENT_NO_TOOL_THRESHOLD_SINGLE if _is_single else WATCHDOG_INTENT_NO_TOOL_THRESHOLD
|
|
14027
|
+
_repeat_th = WATCHDOG_REPEAT_NO_TOOL_THRESHOLD_SINGLE if _is_single else WATCHDOG_REPEAT_NO_TOOL_THRESHOLD
|
|
13545
14028
|
if not bool(dq.get("active", False)):
|
|
13546
|
-
if int(wd.get("intent_no_tool_streak", 0) or 0) >= int(
|
|
14029
|
+
if int(wd.get("intent_no_tool_streak", 0) or 0) >= int(_intent_th):
|
|
13547
14030
|
trigger_reason = "intent-without-tool-call"
|
|
13548
|
-
elif int(wd.get("repeat_no_tool_streak", 0) or 0) >= int(
|
|
14031
|
+
elif int(wd.get("repeat_no_tool_streak", 0) or 0) >= int(_repeat_th):
|
|
13549
14032
|
trigger_reason = "repeated-no-tool-reply"
|
|
13550
14033
|
elif (
|
|
13551
14034
|
self._watchdog_context_near_limit()
|
|
@@ -13564,13 +14047,22 @@ class SessionState:
|
|
|
13564
14047
|
except Exception:
|
|
13565
14048
|
last_trigger_ts = 0.0
|
|
13566
14049
|
if now_ts() - last_trigger_ts >= 1.0:
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
)
|
|
14050
|
+
wd["trigger_count"] = max(0, int(wd.get("trigger_count", 0) or 0)) + 1
|
|
14051
|
+
wd["last_trigger_reason"] = trim(str(trigger_reason or "").strip(), 200)
|
|
14052
|
+
wd["last_trigger_ts"] = float(now_ts())
|
|
14053
|
+
bb["watchdog"] = wd
|
|
14054
|
+
self.blackboard = bb
|
|
14055
|
+
self._blackboard_touch()
|
|
14056
|
+
if int(wd["trigger_count"]) >= 2:
|
|
14057
|
+
self._watchdog_escalate_to_single_developer(bb, reason=trigger_reason)
|
|
14058
|
+
else:
|
|
14059
|
+
triggered = self._watchdog_activate_decomposition(
|
|
14060
|
+
bb,
|
|
14061
|
+
reason=trigger_reason,
|
|
14062
|
+
role=role,
|
|
14063
|
+
step=step,
|
|
14064
|
+
pinned_selection=pinned_selection,
|
|
14065
|
+
)
|
|
13574
14066
|
bb = self._ensure_blackboard()
|
|
13575
14067
|
bb["watchdog"] = self._normalize_watchdog_state(bb.get("watchdog", wd))
|
|
13576
14068
|
bb["decomposition_queue"] = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", dq))
|
|
@@ -13705,6 +14197,7 @@ class SessionState:
|
|
|
13705
14197
|
"text": "",
|
|
13706
14198
|
"ts": 0.0,
|
|
13707
14199
|
},
|
|
14200
|
+
"project_todos": [],
|
|
13708
14201
|
"watchdog": self._new_watchdog_state(),
|
|
13709
14202
|
"decomposition_queue": self._new_decomposition_queue_state(),
|
|
13710
14203
|
}
|
|
@@ -13856,6 +14349,24 @@ class SessionState:
|
|
|
13856
14349
|
"change_count": max(1, int(item.get("change_count", 1) or 1)),
|
|
13857
14350
|
}
|
|
13858
14351
|
board["code_artifacts"] = artifacts
|
|
14352
|
+
if not isinstance(bb_src_todos := src.get("project_todos"), list):
|
|
14353
|
+
board["project_todos"] = []
|
|
14354
|
+
else:
|
|
14355
|
+
clean_todos = []
|
|
14356
|
+
for pt in bb_src_todos[:20]:
|
|
14357
|
+
if not isinstance(pt, dict):
|
|
14358
|
+
continue
|
|
14359
|
+
clean_todos.append({
|
|
14360
|
+
"id": trim(str(pt.get("id", "") or ""), 20),
|
|
14361
|
+
"content": trim(str(pt.get("content", "") or ""), 400),
|
|
14362
|
+
"status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
|
|
14363
|
+
"category": trim(str(pt.get("category", "") or ""), 40),
|
|
14364
|
+
"created_at": float(pt.get("created_at", 0.0) or 0.0),
|
|
14365
|
+
"completed_at": float(pt.get("completed_at", 0.0) or 0.0) if pt.get("completed_at") else None,
|
|
14366
|
+
"completed_by": trim(str(pt.get("completed_by", "") or ""), 40),
|
|
14367
|
+
"evidence": trim(str(pt.get("evidence", "") or ""), 200),
|
|
14368
|
+
})
|
|
14369
|
+
board["project_todos"] = clean_todos
|
|
13859
14370
|
board["watchdog"] = self._normalize_watchdog_state(src.get("watchdog", {}))
|
|
13860
14371
|
board["decomposition_queue"] = self._normalize_decomposition_queue_state(
|
|
13861
14372
|
src.get("decomposition_queue", {})
|
|
@@ -13876,6 +14387,7 @@ class SessionState:
|
|
|
13876
14387
|
def _blackboard_reset_for_goal(self, goal: str):
|
|
13877
14388
|
self.blackboard = self._new_blackboard(goal)
|
|
13878
14389
|
self.manager_context = []
|
|
14390
|
+
self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != "manager"]
|
|
13879
14391
|
self.manager_routes = []
|
|
13880
14392
|
self._blackboard_history("manager", f"new goal accepted: {trim(goal, 300)}")
|
|
13881
14393
|
self._sync_todos_from_blackboard(reason="goal-reset", board=self.blackboard)
|
|
@@ -14186,13 +14698,150 @@ class SessionState:
|
|
|
14186
14698
|
row["activeForm"] = f"Pending ({label}): {text}"
|
|
14187
14699
|
return rows
|
|
14188
14700
|
|
|
14701
|
+
# ── Project-based todo generation & status tracking ──────────────
|
|
14702
|
+
|
|
14703
|
+
def _generate_project_todos_from_profile(self, board: dict | None = None) -> list[dict]:
|
|
14704
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14705
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
14706
|
+
task_type = str(profile.get("task_type", "general") or "general")
|
|
14707
|
+
objective = trim(str(profile.get("direct_objective", "") or ""), 200)
|
|
14708
|
+
|
|
14709
|
+
if task_type == "simple_qa":
|
|
14710
|
+
return [{"content": f"回答: {objective}" if objective else "回答用户问题", "category": "implement"}]
|
|
14711
|
+
|
|
14712
|
+
if task_type in ("simple_code", "engineering"):
|
|
14713
|
+
return [
|
|
14714
|
+
{"content": "分析需求和项目结构", "category": "setup"},
|
|
14715
|
+
{"content": f"实现: {objective}" if objective else "实现编码任务", "category": "implement"},
|
|
14716
|
+
{"content": "编译/语法检查", "category": "compile_test"},
|
|
14717
|
+
{"content": "最小功能测试", "category": "min_test"},
|
|
14718
|
+
]
|
|
14719
|
+
|
|
14720
|
+
if task_type == "research":
|
|
14721
|
+
return [
|
|
14722
|
+
{"content": f"调研: {objective}" if objective else "执行调研任务", "category": "implement"},
|
|
14723
|
+
{"content": "整理调研结果", "category": "review"},
|
|
14724
|
+
]
|
|
14725
|
+
|
|
14726
|
+
return [{"content": f"执行: {objective}" if objective else "执行任务", "category": "implement"}]
|
|
14727
|
+
|
|
14728
|
+
def _init_project_todos(self, board: dict | None = None):
|
|
14729
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14730
|
+
if bb.get("project_todos"):
|
|
14731
|
+
return
|
|
14732
|
+
raw = self._generate_project_todos_from_profile(bb)
|
|
14733
|
+
bb["project_todos"] = [
|
|
14734
|
+
{
|
|
14735
|
+
"id": f"pt:{i:03d}",
|
|
14736
|
+
"content": t["content"],
|
|
14737
|
+
"status": "pending",
|
|
14738
|
+
"category": t["category"],
|
|
14739
|
+
"created_at": float(now_ts()),
|
|
14740
|
+
"completed_at": None,
|
|
14741
|
+
"completed_by": "",
|
|
14742
|
+
"evidence": "",
|
|
14743
|
+
}
|
|
14744
|
+
for i, t in enumerate(raw)
|
|
14745
|
+
]
|
|
14746
|
+
self.blackboard = bb
|
|
14747
|
+
self._blackboard_touch()
|
|
14748
|
+
|
|
14749
|
+
def _has_compile_pass_evidence(self, board: dict | None = None) -> bool:
|
|
14750
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14751
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14752
|
+
if not logs:
|
|
14753
|
+
return False
|
|
14754
|
+
positive = ("compiled successfully", "build successful", "0 error", "编译成功",
|
|
14755
|
+
"syntax ok", "no errors", "build succeeded", "compilation successful")
|
|
14756
|
+
negative = ("error:", "fatal error", "syntax error", "compile error", "build failed")
|
|
14757
|
+
for entry in reversed(logs[-6:]):
|
|
14758
|
+
txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
|
|
14759
|
+
if not txt:
|
|
14760
|
+
continue
|
|
14761
|
+
if any(neg in txt for neg in negative):
|
|
14762
|
+
continue
|
|
14763
|
+
if any(pos in txt for pos in positive):
|
|
14764
|
+
return True
|
|
14765
|
+
return False
|
|
14766
|
+
|
|
14767
|
+
def _has_test_pass_evidence(self, board: dict | None = None) -> bool:
|
|
14768
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14769
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14770
|
+
feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
|
|
14771
|
+
positive = ("test passed", "tests passed", "测试通过", "运行正常",
|
|
14772
|
+
"all tests pass", "ok", "passed", "test succeeded")
|
|
14773
|
+
negative = ("failed", "error", "failure", "测试失败")
|
|
14774
|
+
combined = list(logs[-6:]) + list(feedback[-4:])
|
|
14775
|
+
for entry in reversed(combined[-8:]):
|
|
14776
|
+
txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
|
|
14777
|
+
if not txt:
|
|
14778
|
+
continue
|
|
14779
|
+
if any(neg in txt for neg in negative):
|
|
14780
|
+
continue
|
|
14781
|
+
if any(pos in txt for pos in positive):
|
|
14782
|
+
return True
|
|
14783
|
+
return False
|
|
14784
|
+
|
|
14785
|
+
def _update_project_todo_status(self, board: dict | None = None):
|
|
14786
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14787
|
+
todos = bb.get("project_todos", [])
|
|
14788
|
+
if not todos:
|
|
14789
|
+
return
|
|
14790
|
+
code_count = len(bb.get("code_artifacts", {}) or {})
|
|
14791
|
+
research_count = len(bb.get("research_notes", []) or [])
|
|
14792
|
+
feedback_pass = self._manager_feedback_passed_from_blackboard(bb)
|
|
14793
|
+
|
|
14794
|
+
for todo in todos:
|
|
14795
|
+
if todo.get("status") == "completed":
|
|
14796
|
+
continue
|
|
14797
|
+
cat = todo.get("category", "")
|
|
14798
|
+
if cat == "setup" and (research_count > 0 or code_count > 0):
|
|
14799
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="结构已分析")
|
|
14800
|
+
elif cat == "implement" and code_count > 0:
|
|
14801
|
+
todo.update(status="completed", completed_at=float(now_ts()),
|
|
14802
|
+
completed_by="developer", evidence=f"{code_count} 文件已产出")
|
|
14803
|
+
elif cat == "compile_test" and self._has_compile_pass_evidence(bb):
|
|
14804
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="编译通过")
|
|
14805
|
+
elif cat == "min_test" and self._has_test_pass_evidence(bb):
|
|
14806
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
|
|
14807
|
+
elif cat == "review" and feedback_pass:
|
|
14808
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="审查通过")
|
|
14809
|
+
|
|
14810
|
+
if not any(t.get("status") == "in_progress" for t in todos):
|
|
14811
|
+
for t in todos:
|
|
14812
|
+
if t.get("status") == "pending":
|
|
14813
|
+
t["status"] = "in_progress"
|
|
14814
|
+
break
|
|
14815
|
+
|
|
14816
|
+
bb["project_todos"] = todos
|
|
14817
|
+
self.blackboard = bb
|
|
14818
|
+
|
|
14819
|
+
def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
|
|
14820
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14821
|
+
todos = bb.get("project_todos", [])
|
|
14822
|
+
if not todos:
|
|
14823
|
+
return self._todo_owner_rows_from_blackboard(bb)
|
|
14824
|
+
rows = []
|
|
14825
|
+
for todo in todos:
|
|
14826
|
+
s = todo.get("status", "pending")
|
|
14827
|
+
c = todo.get("content", "")
|
|
14828
|
+
ev = todo.get("evidence", "")
|
|
14829
|
+
af = {
|
|
14830
|
+
"in_progress": f"Working on: {c}",
|
|
14831
|
+
"completed": f"Done: {c}" + (f" ({ev})" if ev else ""),
|
|
14832
|
+
}.get(s, f"Pending: {c}")
|
|
14833
|
+
rows.append({"key": f"bb:proj:{todo.get('id', '')}", "content": c, "status": s, "activeForm": af})
|
|
14834
|
+
return rows
|
|
14835
|
+
|
|
14189
14836
|
def _sync_todos_from_blackboard(self, reason: str = "", board: dict | None = None):
|
|
14190
14837
|
if bool(self.runtime_reclassify_required):
|
|
14191
14838
|
return
|
|
14192
14839
|
if not self._is_multi_agent_mode():
|
|
14193
14840
|
return
|
|
14194
14841
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14195
|
-
|
|
14842
|
+
self._init_project_todos(bb)
|
|
14843
|
+
self._update_project_todo_status(bb)
|
|
14844
|
+
system_rows = self._todo_project_rows_from_blackboard(bb)
|
|
14196
14845
|
existing = self.todo.snapshot()
|
|
14197
14846
|
non_system_rows: list[dict] = []
|
|
14198
14847
|
for row in existing:
|
|
@@ -14200,18 +14849,17 @@ class SessionState:
|
|
|
14200
14849
|
continue
|
|
14201
14850
|
key = str(row.get("key", "") or "").strip()
|
|
14202
14851
|
owner = str(row.get("owner", "") or "").strip().lower()
|
|
14203
|
-
if key.startswith("bb:owner:"
|
|
14852
|
+
if key.startswith(("bb:owner:", "bb:node:", "bb:proj:")) or owner in {"manager", "explorer", "developer", "reviewer"}:
|
|
14204
14853
|
continue
|
|
14205
14854
|
non_system_rows.append(dict(row))
|
|
14206
14855
|
remaining_cap = max(0, 20 - len(system_rows))
|
|
14207
14856
|
merged = list(system_rows) + non_system_rows[:remaining_cap]
|
|
14208
14857
|
try:
|
|
14209
14858
|
todo_out = self.todo.update(merged)
|
|
14210
|
-
except Exception
|
|
14211
|
-
self._emit("status", {"summary": f"owner todo sync skipped: {trim(str(exc), 180)}"})
|
|
14859
|
+
except Exception:
|
|
14212
14860
|
return
|
|
14213
14861
|
if todo_out != "No todo changes." and reason:
|
|
14214
|
-
self._emit("status", {"summary": f"
|
|
14862
|
+
self._emit("status", {"summary": f"project todos synced ({trim(reason, 120)})"})
|
|
14215
14863
|
|
|
14216
14864
|
def _blackboard_set_status(self, status: str, note: str = ""):
|
|
14217
14865
|
board = self._ensure_blackboard()
|
|
@@ -14233,6 +14881,43 @@ class SessionState:
|
|
|
14233
14881
|
self._blackboard_touch()
|
|
14234
14882
|
self._sync_todos_from_blackboard(reason="approved", board=board)
|
|
14235
14883
|
|
|
14884
|
+
def _auto_summary_on_finish(self) -> str:
|
|
14885
|
+
"""Generate concise summary from blackboard state when run ends."""
|
|
14886
|
+
bb = self._ensure_blackboard()
|
|
14887
|
+
artifacts = bb.get("code_artifacts", {}) if isinstance(bb.get("code_artifacts"), dict) else {}
|
|
14888
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14889
|
+
feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
|
|
14890
|
+
summary_parts = []
|
|
14891
|
+
if artifacts:
|
|
14892
|
+
file_list = ", ".join(list(artifacts.keys())[:10])
|
|
14893
|
+
summary_parts.append(f"Modified files: {file_list}")
|
|
14894
|
+
if feedback:
|
|
14895
|
+
last_fb = feedback[-1] if feedback else {}
|
|
14896
|
+
fb_content = str(last_fb.get("content", "") or "") if isinstance(last_fb, dict) else str(last_fb)
|
|
14897
|
+
if fb_content:
|
|
14898
|
+
summary_parts.append(f"Review: {trim(fb_content, 200)}")
|
|
14899
|
+
if logs:
|
|
14900
|
+
recent_logs = logs[-3:]
|
|
14901
|
+
log_strs = []
|
|
14902
|
+
for log_entry in recent_logs:
|
|
14903
|
+
if isinstance(log_entry, dict):
|
|
14904
|
+
log_strs.append(trim(str(log_entry.get("content", "") or ""), 80))
|
|
14905
|
+
elif isinstance(log_entry, str):
|
|
14906
|
+
log_strs.append(trim(log_entry, 80))
|
|
14907
|
+
if log_strs:
|
|
14908
|
+
summary_parts.append(f"Logs: {'; '.join(log_strs)}")
|
|
14909
|
+
summary = "; ".join(summary_parts) or "Task completed."
|
|
14910
|
+
bb["status"] = "COMPLETED"
|
|
14911
|
+
bb["approval"] = {
|
|
14912
|
+
"approved": True,
|
|
14913
|
+
"by": "auto",
|
|
14914
|
+
"note": summary,
|
|
14915
|
+
"ts": float(now_ts()),
|
|
14916
|
+
}
|
|
14917
|
+
self._blackboard_touch()
|
|
14918
|
+
self._emit("status", {"summary": f"[auto-summary] {trim(summary, 400)}"})
|
|
14919
|
+
return summary
|
|
14920
|
+
|
|
14236
14921
|
def _blackboard_read_state_markdown(self, max_items: int = 6) -> str:
|
|
14237
14922
|
board = self._ensure_blackboard()
|
|
14238
14923
|
profile = self._ensure_blackboard_task_profile(board)
|
|
@@ -14347,6 +15032,23 @@ class SessionState:
|
|
|
14347
15032
|
lines.append("- (none)")
|
|
14348
15033
|
_render_tail("Recent Execution Logs", board.get("execution_logs", []))
|
|
14349
15034
|
_render_tail("Recent Review Feedback", board.get("review_feedback", []))
|
|
15035
|
+
|
|
15036
|
+
proj_todos = board.get("project_todos", [])
|
|
15037
|
+
if proj_todos:
|
|
15038
|
+
lines.append("\n### Project Tasks")
|
|
15039
|
+
for pt in proj_todos:
|
|
15040
|
+
s = pt.get("status", "pending")
|
|
15041
|
+
c = trim(str(pt.get("content", "") or ""), 200)
|
|
15042
|
+
ev = trim(str(pt.get("evidence", "") or ""), 100)
|
|
15043
|
+
if s == "completed":
|
|
15044
|
+
mark = "[x]"
|
|
15045
|
+
elif s == "in_progress":
|
|
15046
|
+
mark = "[>]"
|
|
15047
|
+
else:
|
|
15048
|
+
mark = "[ ]"
|
|
15049
|
+
suffix = f" — {ev}" if ev else ""
|
|
15050
|
+
lines.append(f"- {mark} {c}{suffix}")
|
|
15051
|
+
|
|
14350
15052
|
return "\n".join(lines)
|
|
14351
15053
|
|
|
14352
15054
|
def _manager_route_tools(self) -> list[dict]:
|
|
@@ -14612,7 +15314,7 @@ class SessionState:
|
|
|
14612
15314
|
"judgement": trim(str(profile.get("reason", "") or "manager fallback classification"), 200),
|
|
14613
15315
|
"round_budget": int(policy.get("round_budget", profile.get("round_budget", self.max_agent_rounds)) or 0),
|
|
14614
15316
|
"direct_objective": trim(str(profile.get("direct_objective", "") or ""), 800),
|
|
14615
|
-
"execution_mode": str(policy.get("execution_mode", EXECUTION_MODE_SYNC)),
|
|
15317
|
+
"execution_mode": (EXECUTION_MODE_SINGLE if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE else str(policy.get("execution_mode", EXECUTION_MODE_SYNC))),
|
|
14616
15318
|
"participants": participants,
|
|
14617
15319
|
"assigned_expert": assigned,
|
|
14618
15320
|
"requires_user_confirmation": bool(requires_confirmation),
|
|
@@ -14622,7 +15324,7 @@ class SessionState:
|
|
|
14622
15324
|
}
|
|
14623
15325
|
|
|
14624
15326
|
def _manager_classification_system_prompt(self) -> str:
|
|
14625
|
-
|
|
15327
|
+
base = (
|
|
14626
15328
|
"You are Manager. Classify the latest user request by semantic intent, not by keyword templates. "
|
|
14627
15329
|
"Decide whether this latest turn should inherit the previous blackboard/task state. "
|
|
14628
15330
|
"Set inherit_previous_state=true only for genuine follow-up/continuation/refinement of the same ongoing work; "
|
|
@@ -14645,6 +15347,12 @@ class SessionState:
|
|
|
14645
15347
|
"Use low confidence only when semantic ambiguity is substantial, then set low_confidence_reason briefly. "
|
|
14646
15348
|
f"{model_language_instruction(self.ui_language)}"
|
|
14647
15349
|
)
|
|
15350
|
+
if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
|
|
15351
|
+
base += (
|
|
15352
|
+
" NOTE: User has configured Single-agent mode. "
|
|
15353
|
+
"Favor level 1-2 for straightforward tasks; only assign level 3+ when genuine multi-step complexity demands it."
|
|
15354
|
+
)
|
|
15355
|
+
return base
|
|
14648
15356
|
|
|
14649
15357
|
def _apply_runtime_task_decision(self, goal_text: str, decision: dict):
|
|
14650
15358
|
row = dict(decision or {})
|
|
@@ -14666,7 +15374,12 @@ class SessionState:
|
|
|
14666
15374
|
if level not in TASK_LEVEL_CHOICES:
|
|
14667
15375
|
level = 3
|
|
14668
15376
|
policy = dict(TASK_LEVEL_POLICIES.get(level, TASK_LEVEL_POLICIES[3]))
|
|
14669
|
-
|
|
15377
|
+
policy_mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
|
|
15378
|
+
config_mode = normalize_execution_mode(self.execution_mode, default="")
|
|
15379
|
+
if config_mode == EXECUTION_MODE_SINGLE:
|
|
15380
|
+
mode = EXECUTION_MODE_SINGLE
|
|
15381
|
+
else:
|
|
15382
|
+
mode = policy_mode
|
|
14670
15383
|
assigned = self._sanitize_agent_role(
|
|
14671
15384
|
row.get("assigned_expert", policy.get("assigned_expert", "developer"))
|
|
14672
15385
|
) or self._sanitize_agent_role(policy.get("assigned_expert", "developer")) or "developer"
|
|
@@ -14697,6 +15410,9 @@ class SessionState:
|
|
|
14697
15410
|
except Exception:
|
|
14698
15411
|
budget_raw = int(policy.get("round_budget", self.max_agent_rounds) or self.max_agent_rounds)
|
|
14699
15412
|
round_budget = max(1, min(int(self.max_agent_rounds or MAX_AGENT_ROUNDS), int(budget_raw)))
|
|
15413
|
+
if mode == EXECUTION_MODE_SINGLE and policy_mode != EXECUTION_MODE_SINGLE:
|
|
15414
|
+
policy_budget = int(policy.get("round_budget", 10) or 10)
|
|
15415
|
+
round_budget = min(round_budget, max(4, policy_budget // 2))
|
|
14700
15416
|
requires_confirmation = bool(row.get("requires_user_confirmation", policy.get("requires_user_confirmation", False)))
|
|
14701
15417
|
if level == 5 and self._looks_like_positive_confirmation(goal_text):
|
|
14702
15418
|
requires_confirmation = False
|
|
@@ -14875,8 +15591,14 @@ class SessionState:
|
|
|
14875
15591
|
"When user preference is clear, prioritize it over your default plan. "
|
|
14876
15592
|
"Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
|
|
14877
15593
|
"Also decide inherit_previous_state by semantic continuity with prior blackboard state. "
|
|
14878
|
-
"If this is pure continuation, keep current runtime policy unchanged
|
|
15594
|
+
"If this is pure continuation, keep current runtime policy unchanged.\n"
|
|
15595
|
+
f"User configured execution mode: {self.execution_mode}\n"
|
|
14879
15596
|
)
|
|
15597
|
+
if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
|
|
15598
|
+
prompt += (
|
|
15599
|
+
"IMPORTANT: User has configured Single-agent mode. "
|
|
15600
|
+
"Prefer level 1-2 for simple tasks. Only escalate to level 3+ if truly complex.\n"
|
|
15601
|
+
)
|
|
14880
15602
|
with self.lock:
|
|
14881
15603
|
self.current_phase = "manager:classify:model-call"
|
|
14882
15604
|
self.current_tool_name = ""
|
|
@@ -14959,6 +15681,22 @@ class SessionState:
|
|
|
14959
15681
|
self._apply_runtime_task_decision(goal, decision)
|
|
14960
15682
|
return dict(decision or {})
|
|
14961
15683
|
|
|
15684
|
+
def _project_todo_hint_for_manager(self) -> str:
|
|
15685
|
+
bb = self._ensure_blackboard()
|
|
15686
|
+
todos = bb.get("project_todos", [])
|
|
15687
|
+
if not todos:
|
|
15688
|
+
return ""
|
|
15689
|
+
pending = [t for t in todos if t.get("status") != "completed"]
|
|
15690
|
+
if not pending:
|
|
15691
|
+
return "All project tasks completed. Route to finish. "
|
|
15692
|
+
cur = pending[0]
|
|
15693
|
+
cat = cur.get("category", "")
|
|
15694
|
+
if cat == "compile_test":
|
|
15695
|
+
return "NEXT: compile/syntax check required. Route to Developer for build. "
|
|
15696
|
+
if cat == "min_test":
|
|
15697
|
+
return "NEXT: minimal test required. Route to Developer to run tests. "
|
|
15698
|
+
return f"NEXT: {trim(str(cur.get('content', '') or ''), 120)}. "
|
|
15699
|
+
|
|
14962
15700
|
def _manager_system_prompt(self) -> str:
|
|
14963
15701
|
board = self._ensure_blackboard()
|
|
14964
15702
|
profile = self._ensure_blackboard_task_profile(board)
|
|
@@ -14966,41 +15704,30 @@ class SessionState:
|
|
|
14966
15704
|
budget = self._blackboard_round_budget(board)
|
|
14967
15705
|
level = int(profile.get("task_level", self.runtime_task_level or 0) or 0)
|
|
14968
15706
|
mode = normalize_execution_mode(profile.get("execution_mode", self._effective_execution_mode()), default=self._effective_execution_mode())
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
15707
|
+
task_type = str(profile.get("task_type", "general") or "general").strip().lower()
|
|
15708
|
+
coding_hint = ""
|
|
15709
|
+
if task_type in ("simple_code", "engineering"):
|
|
15710
|
+
coding_hint = (
|
|
15711
|
+
"CODING TASK: skip lengthy exploration/design phases. "
|
|
15712
|
+
"Route to Developer early for implementation. "
|
|
15713
|
+
"Explorer should only be used for specific file/API lookups, not broad analysis. "
|
|
15714
|
+
)
|
|
15715
|
+
project_todo_hint = self._project_todo_hint_for_manager()
|
|
14978
15716
|
return (
|
|
14979
|
-
"You are Manager in a
|
|
14980
|
-
"
|
|
14981
|
-
|
|
14982
|
-
"
|
|
14983
|
-
"
|
|
14984
|
-
"
|
|
14985
|
-
"
|
|
14986
|
-
"
|
|
14987
|
-
"
|
|
14988
|
-
"
|
|
14989
|
-
"
|
|
14990
|
-
"
|
|
14991
|
-
"
|
|
14992
|
-
"Decision policy: missing facts/API -> explorer; implementation/update -> developer; "
|
|
14993
|
-
"verification/gap check -> reviewer; only choose finish when review is approved and no blocking logs remain. "
|
|
14994
|
-
"Prefer Manager+AgentBus co-management: when fresh agentbus handoff is available and aligned, "
|
|
14995
|
-
"follow that handoff to reduce orchestration latency instead of re-planning from scratch. "
|
|
14996
|
-
"If finish is blocked by missing final summary after review approval, instruct Reviewer to hand off Explorer "
|
|
14997
|
-
"via agentbus (intent=final_summary_request) instead of silently ending. "
|
|
14998
|
-
f"Current task level={level or '-'}, mode={mode}, scale_preference={scale_preference}, participants={participant_text}, "
|
|
14999
|
-
f"Current task profile: type={profile.get('task_type','general')}, "
|
|
15000
|
-
f"complexity={profile.get('complexity','simple')}, "
|
|
15001
|
-
f"progress={progress}, round_budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
|
|
15717
|
+
"You are Manager in a multi-agent coding system. "
|
|
15718
|
+
"Read blackboard, delegate one short timeslice via route_to_next_agent. "
|
|
15719
|
+
"Policy: missing facts->explorer, implementation->developer, verification->reviewer, "
|
|
15720
|
+
"all done->finish. Set is_mandatory=true when concrete execution is required. "
|
|
15721
|
+
"Role capabilities: "
|
|
15722
|
+
"Explorer=read-only (bash/read_file/search/blackboard, NO write_file/edit_file); "
|
|
15723
|
+
"Developer=all tools (write_file/edit_file/bash/read_file/etc); "
|
|
15724
|
+
"Reviewer=read+verify (bash/read_file/finish_task, NO write_file/edit_file). "
|
|
15725
|
+
"NEVER delegate file-writing tasks to Explorer or Reviewer. "
|
|
15726
|
+
f"{coding_hint}"
|
|
15727
|
+
f"{project_todo_hint}"
|
|
15728
|
+
f"Level={level}, mode={mode}, progress={progress}, "
|
|
15729
|
+
f"budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
|
|
15002
15730
|
f"objective={trim(str(profile.get('direct_objective','') or ''), 220)}. "
|
|
15003
|
-
"Avoid assigning the same agent more than two consecutive turns unless strictly required. "
|
|
15004
15731
|
f"{model_language_instruction(self.ui_language)}"
|
|
15005
15732
|
)
|
|
15006
15733
|
|
|
@@ -15243,6 +15970,24 @@ class SessionState:
|
|
|
15243
15970
|
"reason": "approval-blocked-by-error",
|
|
15244
15971
|
"source": "fallback",
|
|
15245
15972
|
}
|
|
15973
|
+
if finish_gate_reason.startswith("project-todo-incomplete:"):
|
|
15974
|
+
missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
|
|
15975
|
+
if missing_cat == "compile_test":
|
|
15976
|
+
return {
|
|
15977
|
+
"target": "developer",
|
|
15978
|
+
"instruction": "编译/语法检查尚未完成。请编译项目并确认无错误。",
|
|
15979
|
+
"reason": "project-todo-compile-pending",
|
|
15980
|
+
"source": "fallback",
|
|
15981
|
+
"is_mandatory": True,
|
|
15982
|
+
}
|
|
15983
|
+
if missing_cat == "min_test":
|
|
15984
|
+
return {
|
|
15985
|
+
"target": "developer",
|
|
15986
|
+
"instruction": "最小功能测试尚未完成。请运行基本测试验证核心功能。",
|
|
15987
|
+
"reason": "project-todo-test-pending",
|
|
15988
|
+
"source": "fallback",
|
|
15989
|
+
"is_mandatory": True,
|
|
15990
|
+
}
|
|
15246
15991
|
if task_type == "simple_qa":
|
|
15247
15992
|
dev_text = self._latest_agent_assistant_text("developer")
|
|
15248
15993
|
if dev_text:
|
|
@@ -15263,6 +16008,29 @@ class SessionState:
|
|
|
15263
16008
|
"reason": "simple-qa-direct-answer",
|
|
15264
16009
|
"source": "fallback",
|
|
15265
16010
|
}
|
|
16011
|
+
# ── 通用 endpoint 检测:非 simple_qa 的 developer 结论性回复也能触发 finish ──
|
|
16012
|
+
if task_type != "simple_qa":
|
|
16013
|
+
dev_text = self._latest_agent_assistant_text("developer")
|
|
16014
|
+
if dev_text:
|
|
16015
|
+
done_probe = self._detect_endpoint_intent(dev_text, None)
|
|
16016
|
+
if bool(done_probe.get("matched", False)) and not has_error_log:
|
|
16017
|
+
return {
|
|
16018
|
+
"target": "finish",
|
|
16019
|
+
"instruction": "Developer has provided a conclusive response; stop now.",
|
|
16020
|
+
"reason": "general-endpoint-detected",
|
|
16021
|
+
"source": "fallback",
|
|
16022
|
+
}
|
|
16023
|
+
# 通用检查:如果最近的 assistant 消息是结论性回复,且没有待办事项,直接 finish
|
|
16024
|
+
if not has_error_log:
|
|
16025
|
+
for _role in ("developer", "explorer", "reviewer"):
|
|
16026
|
+
_last = self._latest_agent_assistant_text(_role)
|
|
16027
|
+
if _last and self._looks_like_conclusive_reply(_last) and not self.todo.has_open_items():
|
|
16028
|
+
return {
|
|
16029
|
+
"target": "finish",
|
|
16030
|
+
"instruction": "Agent already provided a conclusive reply with no open tasks; stop now.",
|
|
16031
|
+
"reason": "conclusive-reply-detected",
|
|
16032
|
+
"source": "fallback",
|
|
16033
|
+
}
|
|
15266
16034
|
if complexity == "simple" and task_type == "simple_code":
|
|
15267
16035
|
if has_error_log:
|
|
15268
16036
|
return {
|
|
@@ -15398,6 +16166,24 @@ class SessionState:
|
|
|
15398
16166
|
if str(row.get("task_type", "") or "").strip().lower() == "simple_qa":
|
|
15399
16167
|
return row
|
|
15400
16168
|
target = str(row.get("target", "") or "").strip().lower()
|
|
16169
|
+
task_type_low = str(row.get("task_type", "") or "").strip().lower()
|
|
16170
|
+
if task_type_low in ("simple_code", "engineering") and target == "explorer":
|
|
16171
|
+
board = self._ensure_blackboard()
|
|
16172
|
+
progress = self._manager_progress_state(board)
|
|
16173
|
+
if progress in ("initializing", "in_progress"):
|
|
16174
|
+
explorer_count = sum(
|
|
16175
|
+
1 for x in self.manager_routes[-8:]
|
|
16176
|
+
if str(x.get("target", "") or "").strip().lower() == "explorer"
|
|
16177
|
+
)
|
|
16178
|
+
if explorer_count >= 2:
|
|
16179
|
+
row["target"] = "developer"
|
|
16180
|
+
row["instruction"] = (
|
|
16181
|
+
"Coding task: Explorer has been used enough. "
|
|
16182
|
+
"Start implementation now using write_file/edit_file."
|
|
16183
|
+
)
|
|
16184
|
+
row["reason"] = f"{row.get('reason', '')}|coding-fast-track->developer"
|
|
16185
|
+
row["source"] = "anti-stall"
|
|
16186
|
+
return row
|
|
15401
16187
|
if target not in AGENT_ROLES:
|
|
15402
16188
|
return row
|
|
15403
16189
|
recent = [str(x.get("target", "") or "").strip().lower() for x in self.manager_routes[-4:]]
|
|
@@ -15492,6 +16278,11 @@ class SessionState:
|
|
|
15492
16278
|
participants[-1] = target
|
|
15493
16279
|
else:
|
|
15494
16280
|
target = participants[0]
|
|
16281
|
+
# ── Single 模式硬约束:无论 executor_mode_flag 如何,只允许 assigned_expert ──
|
|
16282
|
+
if mode == EXECUTION_MODE_SINGLE:
|
|
16283
|
+
participants = [assigned_expert]
|
|
16284
|
+
if target in AGENT_ROLES and target != assigned_expert:
|
|
16285
|
+
target = assigned_expert
|
|
15495
16286
|
instruction = trim(str(row.get("instruction", "") or "").strip(), 1200)
|
|
15496
16287
|
if not instruction:
|
|
15497
16288
|
instruction = "Proceed with one concrete next step and report evidence."
|
|
@@ -15548,6 +16339,24 @@ class SessionState:
|
|
|
15548
16339
|
feedback_pass = self._manager_feedback_passed_from_blackboard(board)
|
|
15549
16340
|
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
15550
16341
|
force_finish_override = False
|
|
16342
|
+
# ── 结论性回复截断:当 Agent 已回复结论且无待办/无错误时,强制 finish ──
|
|
16343
|
+
if target in AGENT_ROLES and target != "finish":
|
|
16344
|
+
for _check_role in ("developer", "explorer", "reviewer"):
|
|
16345
|
+
_last_text = self._latest_agent_assistant_text(_check_role)
|
|
16346
|
+
if (
|
|
16347
|
+
_last_text
|
|
16348
|
+
and self._looks_like_conclusive_reply(_last_text)
|
|
16349
|
+
and not self.todo.has_open_items()
|
|
16350
|
+
and not self._manager_has_error_log(board)
|
|
16351
|
+
):
|
|
16352
|
+
target = "finish"
|
|
16353
|
+
instruction = (
|
|
16354
|
+
f"Agent '{_check_role}' already provided a conclusive reply. "
|
|
16355
|
+
"No open tasks remain. Finishing now."
|
|
16356
|
+
)
|
|
16357
|
+
row["reason"] = f"conclusive-reply-override:{_check_role}"
|
|
16358
|
+
row["source"] = "policy"
|
|
16359
|
+
break
|
|
15551
16360
|
if bool((board.get("approval", {}) or {}).get("approved", False)) and can_finish_from_approval:
|
|
15552
16361
|
target = "finish"
|
|
15553
16362
|
if not instruction:
|
|
@@ -15616,6 +16425,44 @@ class SessionState:
|
|
|
15616
16425
|
"Do not finish yet. Latest execution logs still contain blocking errors. "
|
|
15617
16426
|
"Resolve errors and provide verifiable evidence."
|
|
15618
16427
|
)
|
|
16428
|
+
elif finish_gate_reason.startswith("project-todo-incomplete:"):
|
|
16429
|
+
missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
|
|
16430
|
+
target = "developer"
|
|
16431
|
+
if missing_cat == "compile_test":
|
|
16432
|
+
instruction = (
|
|
16433
|
+
"编译/语法检查尚未完成。请编译项目并确认无错误。"
|
|
16434
|
+
"Run build/compile and confirm zero errors before finishing."
|
|
16435
|
+
)
|
|
16436
|
+
elif missing_cat == "min_test":
|
|
16437
|
+
instruction = (
|
|
16438
|
+
"最小功能测试尚未完成。请运行基本测试验证核心功能。"
|
|
16439
|
+
"Run minimal tests to verify core functionality before finishing."
|
|
16440
|
+
)
|
|
16441
|
+
else:
|
|
16442
|
+
instruction = (
|
|
16443
|
+
"Project todo not yet completed. Execute the pending step and report evidence."
|
|
16444
|
+
)
|
|
16445
|
+
# Force-finish fallback: if blocked > 3 cycles, mark as done to avoid deadloop
|
|
16446
|
+
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
16447
|
+
if summary_attempts >= 3:
|
|
16448
|
+
force_finish_override = True
|
|
16449
|
+
target = "finish"
|
|
16450
|
+
for pt in board.get("project_todos", []):
|
|
16451
|
+
if pt.get("category") in ("compile_test", "min_test") and pt.get("status") != "completed":
|
|
16452
|
+
pt.update(status="completed", completed_at=float(now_ts()),
|
|
16453
|
+
evidence="force-finish fallback")
|
|
16454
|
+
self.blackboard = board
|
|
16455
|
+
instruction = (
|
|
16456
|
+
"Compile/test gate exceeded retry limit. Force finishing with available evidence. "
|
|
16457
|
+
"Generate final summary and finish now."
|
|
16458
|
+
)
|
|
16459
|
+
row["reason"] = "finish-blocked-project-todo-force-close"
|
|
16460
|
+
row["source"] = "policy"
|
|
16461
|
+
else:
|
|
16462
|
+
board["manager_summary_attempts"] = summary_attempts + 1
|
|
16463
|
+
self.blackboard = board
|
|
16464
|
+
row["reason"] = f"finish-blocked-{missing_cat}"
|
|
16465
|
+
row["source"] = "policy"
|
|
15619
16466
|
else:
|
|
15620
16467
|
has_outputs = bool(code_count > 0 or research_count > 0)
|
|
15621
16468
|
if board_status == "COMPLETED" and has_outputs:
|
|
@@ -15848,7 +16695,7 @@ class SessionState:
|
|
|
15848
16695
|
},
|
|
15849
16696
|
}
|
|
15850
16697
|
]
|
|
15851
|
-
self.
|
|
16698
|
+
self._append_manager_context(
|
|
15852
16699
|
{
|
|
15853
16700
|
"role": "system",
|
|
15854
16701
|
"content": (
|
|
@@ -15858,7 +16705,6 @@ class SessionState:
|
|
|
15858
16705
|
"ts": now_ts(),
|
|
15859
16706
|
}
|
|
15860
16707
|
)
|
|
15861
|
-
self.manager_context = self.manager_context[-400:]
|
|
15862
16708
|
self._emit(
|
|
15863
16709
|
"status",
|
|
15864
16710
|
{
|
|
@@ -15898,7 +16744,7 @@ class SessionState:
|
|
|
15898
16744
|
},
|
|
15899
16745
|
}
|
|
15900
16746
|
]
|
|
15901
|
-
self.
|
|
16747
|
+
self._append_manager_context(
|
|
15902
16748
|
{
|
|
15903
16749
|
"role": "system",
|
|
15904
16750
|
"content": (
|
|
@@ -15908,7 +16754,6 @@ class SessionState:
|
|
|
15908
16754
|
"ts": now_ts(),
|
|
15909
16755
|
}
|
|
15910
16756
|
)
|
|
15911
|
-
self.manager_context = self.manager_context[-400:]
|
|
15912
16757
|
self._emit(
|
|
15913
16758
|
"status",
|
|
15914
16759
|
{
|
|
@@ -15926,8 +16771,8 @@ class SessionState:
|
|
|
15926
16771
|
"Return only one route_to_next_agent call.\n\n"
|
|
15927
16772
|
f"{self._blackboard_read_state_markdown(max_items=6)}"
|
|
15928
16773
|
)
|
|
15929
|
-
self.
|
|
15930
|
-
self.
|
|
16774
|
+
self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
|
|
16775
|
+
self._microcompact_agent_messages(self.manager_context)
|
|
15931
16776
|
with self.lock:
|
|
15932
16777
|
self.current_phase = "manager:model-call"
|
|
15933
16778
|
self.current_tool_name = ""
|
|
@@ -15963,8 +16808,7 @@ class SessionState:
|
|
|
15963
16808
|
}
|
|
15964
16809
|
for tc in tool_calls
|
|
15965
16810
|
]
|
|
15966
|
-
self.
|
|
15967
|
-
self.manager_context = self.manager_context[-400:]
|
|
16811
|
+
self._append_manager_context(assistant)
|
|
15968
16812
|
route_only_tool_calls = False
|
|
15969
16813
|
if isinstance(tool_calls, list) and tool_calls:
|
|
15970
16814
|
tool_names = [
|
|
@@ -16241,6 +17085,9 @@ class SessionState:
|
|
|
16241
17085
|
"agent_role": role_key,
|
|
16242
17086
|
}
|
|
16243
17087
|
self.contexts[role_key] = [executor_seed]
|
|
17088
|
+
# Also filter old messages for this role from agent_messages and add the seed
|
|
17089
|
+
self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != role_key]
|
|
17090
|
+
self.agent_messages.append(executor_seed)
|
|
16244
17091
|
self._emit(
|
|
16245
17092
|
"status",
|
|
16246
17093
|
{
|
|
@@ -16259,11 +17106,22 @@ class SessionState:
|
|
|
16259
17106
|
max_len=1400,
|
|
16260
17107
|
)
|
|
16261
17108
|
language_note = embedded_policy or self._agent_language_policy_note()
|
|
17109
|
+
role_capability_note = {
|
|
17110
|
+
"explorer": "YOUR TOOLS: read-only (bash/read_file/search/blackboard). You CANNOT write_file or edit_file.",
|
|
17111
|
+
"developer": "YOUR TOOLS: all tools available (write_file/edit_file/bash/read_file/etc).",
|
|
17112
|
+
"reviewer": "YOUR TOOLS: read+verify (bash/read_file/finish_task). You CANNOT write_file or edit_file.",
|
|
17113
|
+
}.get(role_key, "")
|
|
17114
|
+
if role_key == "explorer":
|
|
17115
|
+
tool_examples = "bash/read_file/read_from_blackboard"
|
|
17116
|
+
elif role_key == "reviewer":
|
|
17117
|
+
tool_examples = "bash/read_file/finish_task"
|
|
17118
|
+
else:
|
|
17119
|
+
tool_examples = "write_file/edit_file/bash/read_file"
|
|
16262
17120
|
mandatory_note = (
|
|
16263
17121
|
(
|
|
16264
17122
|
"MANDATORY EXECUTION: this delegate is hard-push. "
|
|
16265
17123
|
"You must call at least one concrete tool in this turn "
|
|
16266
|
-
"(e.g.
|
|
17124
|
+
f"(e.g. {tool_examples}) and produce verifiable progress. "
|
|
16267
17125
|
"Suggestion-only text reply is forbidden."
|
|
16268
17126
|
)
|
|
16269
17127
|
if bool(is_mandatory)
|
|
@@ -16293,6 +17151,7 @@ class SessionState:
|
|
|
16293
17151
|
f"{mandatory_note}\n"
|
|
16294
17152
|
f"{executor_note}\n"
|
|
16295
17153
|
f"{collaboration_note}\n"
|
|
17154
|
+
f"{role_capability_note}\n"
|
|
16296
17155
|
"</manager-delegate>\n"
|
|
16297
17156
|
"<blackboard-state>\n"
|
|
16298
17157
|
f"{trim(board_md, 6000)}\n"
|
|
@@ -16802,10 +17661,17 @@ class SessionState:
|
|
|
16802
17661
|
role_key = self._sanitize_agent_role(role)
|
|
16803
17662
|
if not role_key:
|
|
16804
17663
|
return self.messages
|
|
17664
|
+
# Build filtered view from unified agent_messages
|
|
17665
|
+
filtered = [
|
|
17666
|
+
m for m in self.agent_messages
|
|
17667
|
+
if m.get("agent_role") == role_key
|
|
17668
|
+
or m.get("agent_role") == "shared"
|
|
17669
|
+
or (m.get("role") == "user" and not m.get("agent_role"))
|
|
17670
|
+
]
|
|
17671
|
+
# Update legacy cache for backward compatibility (serialization, etc.)
|
|
16805
17672
|
if not isinstance(self.contexts, dict):
|
|
16806
17673
|
self.contexts = {r: [] for r in AGENT_ROLES}
|
|
16807
|
-
|
|
16808
|
-
self.contexts[role_key] = []
|
|
17674
|
+
self.contexts[role_key] = filtered[-400:]
|
|
16809
17675
|
return self.contexts[role_key]
|
|
16810
17676
|
|
|
16811
17677
|
def _append_agent_context_message(self, role: str, message: dict, *, mirror_to_global: bool = False) -> dict:
|
|
@@ -16814,11 +17680,10 @@ class SessionState:
|
|
|
16814
17680
|
row["agent_role"] = role_key
|
|
16815
17681
|
if "ts" not in row:
|
|
16816
17682
|
row["ts"] = now_ts()
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
if len(
|
|
16820
|
-
self.
|
|
16821
|
-
ctx = self.contexts[role_key]
|
|
17683
|
+
# Write to unified agent_messages
|
|
17684
|
+
self.agent_messages.append(row)
|
|
17685
|
+
if len(self.agent_messages) > 1200:
|
|
17686
|
+
self.agent_messages = self.agent_messages[-800:]
|
|
16822
17687
|
if mirror_to_global:
|
|
16823
17688
|
mirror = dict(row)
|
|
16824
17689
|
if "tool_calls" in mirror and isinstance(mirror.get("tool_calls"), list):
|
|
@@ -16838,6 +17703,19 @@ class SessionState:
|
|
|
16838
17703
|
self.messages = self.messages[-400:]
|
|
16839
17704
|
return row
|
|
16840
17705
|
|
|
17706
|
+
def _append_manager_context(self, message: dict):
|
|
17707
|
+
"""Append to manager_context and agent_messages in sync."""
|
|
17708
|
+
row = dict(message or {})
|
|
17709
|
+
if "agent_role" not in row:
|
|
17710
|
+
row["agent_role"] = "manager"
|
|
17711
|
+
if "ts" not in row:
|
|
17712
|
+
row["ts"] = now_ts()
|
|
17713
|
+
self.manager_context.append(row)
|
|
17714
|
+
self.manager_context = self.manager_context[-400:]
|
|
17715
|
+
self.agent_messages.append(row)
|
|
17716
|
+
if len(self.agent_messages) > 1200:
|
|
17717
|
+
self.agent_messages = self.agent_messages[-800:]
|
|
17718
|
+
|
|
16841
17719
|
def _agent_display_name(self, role: str) -> str:
|
|
16842
17720
|
return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
|
|
16843
17721
|
|
|
@@ -16935,52 +17813,69 @@ class SessionState:
|
|
|
16935
17813
|
)
|
|
16936
17814
|
return envelope
|
|
16937
17815
|
|
|
17816
|
+
def _drain_agentbus_fast_route(self) -> dict | None:
|
|
17817
|
+
"""Check agent_bus_messages for an unprocessed handoff that can skip manager.
|
|
17818
|
+
|
|
17819
|
+
Returns route dict with 'to' and 'payload' if a valid fast-route is found,
|
|
17820
|
+
otherwise returns None (fall back to manager delegation).
|
|
17821
|
+
"""
|
|
17822
|
+
if not self.agent_bus_messages:
|
|
17823
|
+
return None
|
|
17824
|
+
now = now_ts()
|
|
17825
|
+
valid_intents = {
|
|
17826
|
+
"handoff", "review_request", "fix_request",
|
|
17827
|
+
"final_summary_request", "implementation_ready",
|
|
17828
|
+
}
|
|
17829
|
+
for env in reversed(self.agent_bus_messages[-20:]):
|
|
17830
|
+
if not isinstance(env, dict):
|
|
17831
|
+
continue
|
|
17832
|
+
if env.get("_fast_routed"):
|
|
17833
|
+
continue
|
|
17834
|
+
intent = str(env.get("intent", "") or "").strip().lower()
|
|
17835
|
+
if intent not in valid_intents:
|
|
17836
|
+
continue
|
|
17837
|
+
to_role = self._sanitize_agent_role(env.get("to", ""))
|
|
17838
|
+
if not to_role or to_role not in AGENT_ROLES:
|
|
17839
|
+
continue
|
|
17840
|
+
age = max(0.0, now - float(env.get("ts", 0.0) or 0.0))
|
|
17841
|
+
if age > 180.0:
|
|
17842
|
+
continue
|
|
17843
|
+
env["_fast_routed"] = True
|
|
17844
|
+
payload = trim(str(env.get("payload", "") or ""), 1400)
|
|
17845
|
+
from_role = str(env.get("from", "") or "")
|
|
17846
|
+
self._emit(
|
|
17847
|
+
"status",
|
|
17848
|
+
{
|
|
17849
|
+
"summary": (
|
|
17850
|
+
f"agentbus fast-route: {from_role}->{to_role} "
|
|
17851
|
+
f"intent={intent} (skipping manager)"
|
|
17852
|
+
)
|
|
17853
|
+
},
|
|
17854
|
+
)
|
|
17855
|
+
return {
|
|
17856
|
+
"to": to_role,
|
|
17857
|
+
"payload": payload,
|
|
17858
|
+
"intent": intent,
|
|
17859
|
+
"from": from_role,
|
|
17860
|
+
"env_id": env.get("id", ""),
|
|
17861
|
+
}
|
|
17862
|
+
return None
|
|
17863
|
+
|
|
16938
17864
|
def _agent_role_system_prompt(self, role: str) -> str:
|
|
16939
17865
|
role_key = self._sanitize_agent_role(role) or "developer"
|
|
16940
17866
|
base = (
|
|
16941
|
-
f"You are {self._agent_display_name(role_key)} in a
|
|
16942
|
-
f"Workspace
|
|
16943
|
-
"
|
|
16944
|
-
"
|
|
16945
|
-
|
|
16946
|
-
"Use relative file paths (for example hello.txt); runtime maps them to session absolute paths. "
|
|
16947
|
-
"If '/workspace/...' appears, treat it as a virtual alias only; never create OS-level /workspace in shell. "
|
|
16948
|
-
f"{_detect_os_shell_instruction()} "
|
|
16949
|
-
"You must stay within your role boundary and use only provided tools. "
|
|
16950
|
-
"Use read_from_blackboard/write_to_blackboard to keep the shared state accurate. "
|
|
16951
|
-
"When communicating with other agents, use ask_colleague with structured intent/content. "
|
|
16952
|
-
"Always keep outputs concise and action-oriented. "
|
|
17867
|
+
f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
|
|
17868
|
+
f"Workspace: {self.files_root}. Use relative paths. "
|
|
17869
|
+
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
17870
|
+
"Keep outputs concise and action-oriented. "
|
|
17871
|
+
"Use load_skill for detailed guidance (multi-agent-guide, code-review-checklist, finish-protocol). "
|
|
16953
17872
|
f"{model_language_instruction(self.ui_language)} "
|
|
16954
17873
|
)
|
|
16955
17874
|
if role_key == "explorer":
|
|
16956
|
-
return
|
|
16957
|
-
base
|
|
16958
|
-
+ "Role objective: analyze user goals, inspect codebase, and produce actionable research notes. "
|
|
16959
|
-
+ "Prefer read/search/check commands; avoid direct large code modifications. "
|
|
16960
|
-
+ "When new evidence appears, write concise research updates to blackboard and hand off actionable insights. "
|
|
16961
|
-
+ "Proactively use ask_colleague when your findings can unblock developer/reviewer immediately. "
|
|
16962
|
-
+ "If reviewer sends final_summary_request, produce final wrap-up summary from blackboard evidence and finish."
|
|
16963
|
-
)
|
|
17875
|
+
return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
|
|
16964
17876
|
if role_key == "reviewer":
|
|
16965
|
-
return
|
|
16966
|
-
|
|
16967
|
-
+ "Role objective: verify developer output against goal, run checks/tests, and issue pass/fix decisions. "
|
|
16968
|
-
+ "If gaps remain, send fix_request to developer with concrete failure evidence and write review_feedback to blackboard. "
|
|
16969
|
-
+ "If manager requests final summary, first call read_from_blackboard "
|
|
16970
|
-
+ "(sections: code_artifacts, execution_logs, review_feedback, status), then generate a structured summary "
|
|
16971
|
-
+ "covering changes, validation evidence, and residual risks/next steps. "
|
|
16972
|
-
+ "When finishing, pass this summary in finish_task.summary; empty or vague summary is invalid. "
|
|
16973
|
-
+ "If you cannot produce summary from current evidence, hand off Explorer via ask_colleague "
|
|
16974
|
-
+ "intent=final_summary_request with explicit missing evidence."
|
|
16975
|
-
)
|
|
16976
|
-
return (
|
|
16977
|
-
base
|
|
16978
|
-
+ "Role objective: implement code changes based on explorer/reviewer inputs. "
|
|
16979
|
-
+ "Perform concrete file edits and command execution. "
|
|
16980
|
-
+ "Continuously record progress and blockers to blackboard. "
|
|
16981
|
-
+ "When blocked or uncertain, immediately call ask_colleague to explorer/reviewer with focused intent. "
|
|
16982
|
-
+ "When implementation batch is ready, send review_request to reviewer via ask_colleague."
|
|
16983
|
-
)
|
|
17877
|
+
return base + "Role: verify output, run checks, issue pass/fix decisions. Write review_feedback to blackboard. "
|
|
17878
|
+
return base + "Role: implement code changes, execute tools, record progress to blackboard. "
|
|
16984
17879
|
|
|
16985
17880
|
def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
|
|
16986
17881
|
if not self._is_multi_agent_mode():
|
|
@@ -17005,16 +17900,17 @@ class SessionState:
|
|
|
17005
17900
|
mirror_to_global=False,
|
|
17006
17901
|
)
|
|
17007
17902
|
if not self.manager_context:
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
"
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
]
|
|
17903
|
+
init_msg = {
|
|
17904
|
+
"role": "system",
|
|
17905
|
+
"content": (
|
|
17906
|
+
"Manager context initialized. Delegate by reading blackboard and assigning short slices.\n"
|
|
17907
|
+
f"{language_note}"
|
|
17908
|
+
),
|
|
17909
|
+
"ts": now_ts(),
|
|
17910
|
+
"agent_role": "manager",
|
|
17911
|
+
}
|
|
17912
|
+
self.manager_context = [init_msg]
|
|
17913
|
+
self.agent_messages.append(init_msg)
|
|
17018
17914
|
if not self._agent_context("developer"):
|
|
17019
17915
|
self._append_agent_context_message(
|
|
17020
17916
|
"developer",
|
|
@@ -17706,11 +18602,9 @@ class SessionState:
|
|
|
17706
18602
|
self._emit("status", {"summary": summary})
|
|
17707
18603
|
out = f"{out}\n{summary}"
|
|
17708
18604
|
after_text = try_read_text(fp, max_bytes=CODE_PREVIEW_STAGE_MAX_BYTES) or ""
|
|
17709
|
-
diff = make_unified_diff(rel, before_text, after_text)
|
|
18605
|
+
diff, added, deleted = make_unified_diff(rel, before_text, after_text)
|
|
17710
18606
|
numbered = make_numbered_diff(before_text, after_text)
|
|
17711
18607
|
numbered_text = render_numbered_diff_text(numbered)
|
|
17712
|
-
added = sum(1 for ln in diff.splitlines() if ln.startswith("+") and not ln.startswith("+++"))
|
|
17713
|
-
deleted = sum(1 for ln in diff.splitlines() if ln.startswith("-") and not ln.startswith("---"))
|
|
17714
18608
|
code_stage = self._record_code_preview_stage(
|
|
17715
18609
|
rel_path=rel,
|
|
17716
18610
|
before_text=before_text,
|
|
@@ -17759,11 +18653,9 @@ class SessionState:
|
|
|
17759
18653
|
self._emit("status", {"summary": summary})
|
|
17760
18654
|
out = f"{out}\n{summary}"
|
|
17761
18655
|
after_text = try_read_text(fp, max_bytes=CODE_PREVIEW_STAGE_MAX_BYTES) or ""
|
|
17762
|
-
diff = make_unified_diff(rel, before_text, after_text)
|
|
18656
|
+
diff, added, deleted = make_unified_diff(rel, before_text, after_text)
|
|
17763
18657
|
numbered = make_numbered_diff(before_text, after_text)
|
|
17764
18658
|
numbered_text = render_numbered_diff_text(numbered)
|
|
17765
|
-
added = sum(1 for ln in diff.splitlines() if ln.startswith("+") and not ln.startswith("+++"))
|
|
17766
|
-
deleted = sum(1 for ln in diff.splitlines() if ln.startswith("-") and not ln.startswith("---"))
|
|
17767
18659
|
code_stage = self._record_code_preview_stage(
|
|
17768
18660
|
rel_path=rel,
|
|
17769
18661
|
before_text=before_text,
|
|
@@ -18340,6 +19232,7 @@ class SessionState:
|
|
|
18340
19232
|
ctx = self._agent_context(role_key)
|
|
18341
19233
|
if not ctx:
|
|
18342
19234
|
return {"status": "skip", "reason": "empty-context", "role": role_key}
|
|
19235
|
+
self._microcompact_agent_messages(ctx)
|
|
18343
19236
|
with self.lock:
|
|
18344
19237
|
self.current_phase = f"agent:{role_key}:model-call"
|
|
18345
19238
|
self.current_tool_name = ""
|
|
@@ -18348,7 +19241,7 @@ class SessionState:
|
|
|
18348
19241
|
ctx,
|
|
18349
19242
|
tools=self._tools_for_agent(role_key),
|
|
18350
19243
|
system=self._agent_role_system_prompt(role_key),
|
|
18351
|
-
max_tokens=
|
|
19244
|
+
max_tokens=self.max_output_tokens,
|
|
18352
19245
|
think=False,
|
|
18353
19246
|
stream_thinking=False,
|
|
18354
19247
|
on_thinking_chunk=self._append_live_thinking,
|
|
@@ -18388,7 +19281,7 @@ class SessionState:
|
|
|
18388
19281
|
for tc in tool_calls
|
|
18389
19282
|
]
|
|
18390
19283
|
self._append_agent_context_message(role_key, assistant, mirror_to_global=True)
|
|
18391
|
-
if text.strip() or thinking_text:
|
|
19284
|
+
if (text.strip() or thinking_text) and not tool_calls:
|
|
18392
19285
|
emit_text = text if text.strip() else "[thinking-only output]"
|
|
18393
19286
|
self._emit_agent_message(role_key, emit_text, summary=f"{self._agent_display_name(role_key)} response")
|
|
18394
19287
|
if not tool_calls:
|
|
@@ -18694,10 +19587,23 @@ class SessionState:
|
|
|
18694
19587
|
media_inputs_pool=media_inputs_pool,
|
|
18695
19588
|
media_seen_ts_by_role=media_seen_ts_by_role,
|
|
18696
19589
|
)
|
|
18697
|
-
route
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
19590
|
+
# AgentBus fast-route: skip manager if a valid worker handoff is pending
|
|
19591
|
+
bus_route = self._drain_agentbus_fast_route()
|
|
19592
|
+
if bus_route:
|
|
19593
|
+
target = bus_route["to"]
|
|
19594
|
+
instruction = trim(str(bus_route.get("payload", "") or ""), 1400)
|
|
19595
|
+
route = {
|
|
19596
|
+
"target": target,
|
|
19597
|
+
"instruction": instruction,
|
|
19598
|
+
"source": "agentbus-direct",
|
|
19599
|
+
"is_mandatory": False,
|
|
19600
|
+
"executor_mode": False,
|
|
19601
|
+
}
|
|
19602
|
+
else:
|
|
19603
|
+
route = self._manager_delegate_turn(
|
|
19604
|
+
pinned_selection=pinned_selection,
|
|
19605
|
+
media_inputs_round=manager_media_inputs,
|
|
19606
|
+
)
|
|
18701
19607
|
target = str(route.get("target", "") or "").strip().lower()
|
|
18702
19608
|
instruction = trim(str(route.get("instruction", "") or "").strip(), 1400)
|
|
18703
19609
|
if compact_mode and target in AGENT_ROLES:
|
|
@@ -18738,6 +19644,23 @@ class SessionState:
|
|
|
18738
19644
|
media_inputs_round=role_media_inputs,
|
|
18739
19645
|
)
|
|
18740
19646
|
self._blackboard_update_from_worker_step(role, step)
|
|
19647
|
+
# ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
|
|
19648
|
+
agent_text = self._latest_agent_assistant_text(role)
|
|
19649
|
+
if (
|
|
19650
|
+
agent_text
|
|
19651
|
+
and self._looks_like_conclusive_reply(agent_text)
|
|
19652
|
+
and not self.todo.has_open_items()
|
|
19653
|
+
and not self._manager_has_error_log(self._ensure_blackboard())
|
|
19654
|
+
):
|
|
19655
|
+
self._blackboard_mark_approved(
|
|
19656
|
+
f"conclusive reply from {role}: auto-finish", role
|
|
19657
|
+
)
|
|
19658
|
+
self._mark_all_done_silently(f"conclusive reply from {role}")
|
|
19659
|
+
self._emit(
|
|
19660
|
+
"status",
|
|
19661
|
+
{"summary": f"agent '{role}' gave conclusive reply; finishing run"},
|
|
19662
|
+
)
|
|
19663
|
+
break
|
|
18741
19664
|
board_after = self._ensure_blackboard()
|
|
18742
19665
|
board_after_fp = self._watchdog_state_fingerprint(board_after)
|
|
18743
19666
|
wd_event = self._watchdog_process_worker_step(
|
|
@@ -18786,6 +19709,7 @@ class SessionState:
|
|
|
18786
19709
|
},
|
|
18787
19710
|
)
|
|
18788
19711
|
continue
|
|
19712
|
+
self._auto_summary_on_finish()
|
|
18789
19713
|
self._mark_all_done_silently(note)
|
|
18790
19714
|
self._emit(
|
|
18791
19715
|
"status",
|
|
@@ -18847,7 +19771,9 @@ class SessionState:
|
|
|
18847
19771
|
)
|
|
18848
19772
|
break
|
|
18849
19773
|
else:
|
|
18850
|
-
|
|
19774
|
+
summary = self._auto_summary_on_finish()
|
|
19775
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19776
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
18851
19777
|
|
|
18852
19778
|
def _multi_agent_worker(self, *, pinned_selection: str):
|
|
18853
19779
|
mode = self._effective_execution_mode()
|
|
@@ -18980,7 +19906,9 @@ class SessionState:
|
|
|
18980
19906
|
sync_index += 1
|
|
18981
19907
|
continue
|
|
18982
19908
|
else:
|
|
18983
|
-
|
|
19909
|
+
summary = self._auto_summary_on_finish()
|
|
19910
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19911
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
18984
19912
|
|
|
18985
19913
|
def _agent_worker(self):
|
|
18986
19914
|
single_role = "developer"
|
|
@@ -19205,7 +20133,7 @@ class SessionState:
|
|
|
19205
20133
|
self.messages,
|
|
19206
20134
|
tools=TOOLS,
|
|
19207
20135
|
system=self._system_prompt(),
|
|
19208
|
-
max_tokens=
|
|
20136
|
+
max_tokens=self.max_output_tokens,
|
|
19209
20137
|
think=False,
|
|
19210
20138
|
stream_thinking=False,
|
|
19211
20139
|
on_thinking_chunk=self._append_live_thinking,
|
|
@@ -19339,7 +20267,7 @@ class SessionState:
|
|
|
19339
20267
|
for tc in tool_calls
|
|
19340
20268
|
]
|
|
19341
20269
|
self.messages.append(assistant)
|
|
19342
|
-
if text.strip() or thinking_text:
|
|
20270
|
+
if (text.strip() or thinking_text) and not tool_calls:
|
|
19343
20271
|
emit_text = text if text.strip() else "[thinking-only output]"
|
|
19344
20272
|
emit_summary = "assistant message" if text.strip() else "assistant thinking-only message"
|
|
19345
20273
|
self._emit(
|
|
@@ -19497,6 +20425,8 @@ class SessionState:
|
|
|
19497
20425
|
fault_counter = 0
|
|
19498
20426
|
last_fault_reason = ""
|
|
19499
20427
|
self._prune_runtime_retry_hints()
|
|
20428
|
+
if self.todo.has_open_items():
|
|
20429
|
+
self._mark_all_done_silently("single-mode endpoint exit")
|
|
19500
20430
|
self._emit(
|
|
19501
20431
|
"status",
|
|
19502
20432
|
{
|
|
@@ -19514,6 +20444,8 @@ class SessionState:
|
|
|
19514
20444
|
fault_counter = 0
|
|
19515
20445
|
last_fault_reason = ""
|
|
19516
20446
|
self._prune_runtime_retry_hints()
|
|
20447
|
+
if self.todo.has_open_items():
|
|
20448
|
+
self._mark_all_done_silently("single-mode conclusive/substantial exit")
|
|
19517
20449
|
self._emit(
|
|
19518
20450
|
"status",
|
|
19519
20451
|
{
|
|
@@ -19579,6 +20511,9 @@ class SessionState:
|
|
|
19579
20511
|
},
|
|
19580
20512
|
)
|
|
19581
20513
|
continue
|
|
20514
|
+
# 对简单查询(非工程任务)限制自动继续预算
|
|
20515
|
+
if auto_continue_budget > 8 and not self._is_long_running_engineering_context():
|
|
20516
|
+
auto_continue_budget = min(auto_continue_budget, 8)
|
|
19582
20517
|
can_continue = auto_continue_budget > 0 and (
|
|
19583
20518
|
todo_blocking or self._looks_like_incomplete_reply(text)
|
|
19584
20519
|
)
|
|
@@ -20080,7 +21015,9 @@ class SessionState:
|
|
|
20080
21015
|
if stop_due_to_repeated_tool_loop or stop_due_to_hard_break or stop_due_to_finish_task:
|
|
20081
21016
|
break
|
|
20082
21017
|
else:
|
|
20083
|
-
|
|
21018
|
+
summary = self._auto_summary_on_finish()
|
|
21019
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
21020
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
20084
21021
|
except CircuitBreakerTriggered as exc:
|
|
20085
21022
|
note = trim(str(exc), 320) or "Circuit breaker triggered."
|
|
20086
21023
|
self._emit("status", {"summary": f"hard-stop: {note}"})
|
|
@@ -20097,6 +21034,10 @@ class SessionState:
|
|
|
20097
21034
|
except Exception as exc:
|
|
20098
21035
|
self._emit("error", {"summary": f"agent error: {exc}", "trace": traceback.format_exc()})
|
|
20099
21036
|
finally:
|
|
21037
|
+
if self.todo.has_open_items() and not self.cancel_requested:
|
|
21038
|
+
_last = self._latest_agent_assistant_text(single_role) or ""
|
|
21039
|
+
if self._looks_like_conclusive_reply(_last):
|
|
21040
|
+
self._mark_all_done_silently(f"single-mode conclusive exit by {single_role}")
|
|
20100
21041
|
dropped_pending_inputs = 0
|
|
20101
21042
|
removed_runtime_hints = 0
|
|
20102
21043
|
with self.lock:
|
|
@@ -20446,6 +21387,107 @@ class SessionState:
|
|
|
20446
21387
|
bio.seek(0)
|
|
20447
21388
|
return bio.read()
|
|
20448
21389
|
|
|
21390
|
+
def export_conversation_md(self) -> str:
|
|
21391
|
+
snap = self.snapshot()
|
|
21392
|
+
title = snap.get("title") or snap.get("id") or "Session"
|
|
21393
|
+
lines = [
|
|
21394
|
+
f"# {title}",
|
|
21395
|
+
"",
|
|
21396
|
+
f"- Session: `{snap.get('id', '')}`",
|
|
21397
|
+
f"- Model: `{snap.get('model', '')}`",
|
|
21398
|
+
f"- Created: {_fmt_export_ts(snap.get('created_at', 0))}",
|
|
21399
|
+
"",
|
|
21400
|
+
"---",
|
|
21401
|
+
"",
|
|
21402
|
+
]
|
|
21403
|
+
for row in snap.get("conversation_feed", []):
|
|
21404
|
+
role = str(row.get("role", "system"))
|
|
21405
|
+
ts = row.get("ts", 0)
|
|
21406
|
+
time_str = _fmt_export_ts(ts)
|
|
21407
|
+
text = str(row.get("text", ""))
|
|
21408
|
+
thinking = str(row.get("thinking", "") or "")
|
|
21409
|
+
row_type = str(row.get("type", "message"))
|
|
21410
|
+
agent = str(row.get("agent_role", "") or "")
|
|
21411
|
+
header = f"**[{role}]**"
|
|
21412
|
+
if agent:
|
|
21413
|
+
header += f" _{agent}_"
|
|
21414
|
+
if row_type not in ("message", ""):
|
|
21415
|
+
header += f" `{row_type}`"
|
|
21416
|
+
if time_str:
|
|
21417
|
+
header += f" <sub>{time_str}</sub>"
|
|
21418
|
+
lines.append(header)
|
|
21419
|
+
lines.append("")
|
|
21420
|
+
if thinking:
|
|
21421
|
+
lines.append("<details><summary>thinking</summary>")
|
|
21422
|
+
lines.append("")
|
|
21423
|
+
lines.append(thinking)
|
|
21424
|
+
lines.append("")
|
|
21425
|
+
lines.append("</details>")
|
|
21426
|
+
lines.append("")
|
|
21427
|
+
if text:
|
|
21428
|
+
lines.append(text)
|
|
21429
|
+
lines.append("")
|
|
21430
|
+
lines.append("---")
|
|
21431
|
+
lines.append("")
|
|
21432
|
+
return "\n".join(lines)
|
|
21433
|
+
|
|
21434
|
+
def export_conversation_pdf(self) -> bytes:
|
|
21435
|
+
md_text = self.export_conversation_md()
|
|
21436
|
+
return _text_to_minimal_pdf(md_text)
|
|
21437
|
+
|
|
21438
|
+
def _conversation_to_html(self) -> str:
|
|
21439
|
+
snap = self.snapshot()
|
|
21440
|
+
title = _html_esc(snap.get("title") or snap.get("id") or "Session")
|
|
21441
|
+
model = _html_esc(snap.get("model", ""))
|
|
21442
|
+
rows_html = []
|
|
21443
|
+
for row in snap.get("conversation_feed", []):
|
|
21444
|
+
role = str(row.get("role", "system"))
|
|
21445
|
+
ts = row.get("ts", 0)
|
|
21446
|
+
time_str = _fmt_export_ts(ts)
|
|
21447
|
+
text = str(row.get("text", ""))
|
|
21448
|
+
thinking = str(row.get("thinking", "") or "")
|
|
21449
|
+
bg = "#e8f4fd" if role == "user" else ("#f0f0f0" if role == "assistant" else "#fff9e6")
|
|
21450
|
+
block = f'<div style="background:{bg};border-radius:8px;padding:10px 14px;margin:6px 0">'
|
|
21451
|
+
block += f'<div style="font-weight:bold;font-size:13px;color:#555;margin-bottom:4px">[{_html_esc(role)}] {_html_esc(time_str)}</div>'
|
|
21452
|
+
if thinking:
|
|
21453
|
+
block += f'<details style="margin-bottom:6px"><summary style="color:#888;font-size:12px">thinking</summary><pre style="white-space:pre-wrap;font-size:12px;color:#666">{_html_esc(thinking)}</pre></details>'
|
|
21454
|
+
if text:
|
|
21455
|
+
block += f'<pre style="white-space:pre-wrap;font-size:13px;margin:0">{_html_esc(text)}</pre>'
|
|
21456
|
+
block += '</div>'
|
|
21457
|
+
rows_html.append(block)
|
|
21458
|
+
body = "\n".join(rows_html)
|
|
21459
|
+
return f"""<!DOCTYPE html>
|
|
21460
|
+
<html><head><meta charset="utf-8"><title>{title}</title>
|
|
21461
|
+
<style>body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:860px;margin:0 auto;padding:20px;background:#fff}}
|
|
21462
|
+
h1{{font-size:20px;margin-bottom:4px}}
|
|
21463
|
+
.meta{{color:#888;font-size:13px;margin-bottom:16px}}</style></head>
|
|
21464
|
+
<body><h1>{title}</h1><div class="meta">Model: {model}</div>
|
|
21465
|
+
{body}
|
|
21466
|
+
</body></html>"""
|
|
21467
|
+
|
|
21468
|
+
def export_conversation_image(self) -> bytes:
|
|
21469
|
+
html_content = self._conversation_to_html()
|
|
21470
|
+
try:
|
|
21471
|
+
from playwright.sync_api import sync_playwright
|
|
21472
|
+
with sync_playwright() as p:
|
|
21473
|
+
browser = p.chromium.launch()
|
|
21474
|
+
page = browser.new_page(viewport={"width": 860, "height": 800})
|
|
21475
|
+
page.set_content(html_content)
|
|
21476
|
+
page.wait_for_load_state("networkidle")
|
|
21477
|
+
img = page.screenshot(full_page=True, type="png")
|
|
21478
|
+
browser.close()
|
|
21479
|
+
return img
|
|
21480
|
+
except ImportError:
|
|
21481
|
+
pass
|
|
21482
|
+
except Exception:
|
|
21483
|
+
pass
|
|
21484
|
+
try:
|
|
21485
|
+
import imgkit
|
|
21486
|
+
return imgkit.from_string(html_content, False, options={"width": "860", "encoding": "UTF-8"})
|
|
21487
|
+
except ImportError:
|
|
21488
|
+
pass
|
|
21489
|
+
raise RuntimeError("Image export requires playwright or imgkit. Install: pip install playwright && playwright install chromium")
|
|
21490
|
+
|
|
20449
21491
|
class SessionManager:
|
|
20450
21492
|
def __init__(
|
|
20451
21493
|
self,
|
|
@@ -20471,6 +21513,7 @@ class SessionManager:
|
|
|
20471
21513
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
20472
21514
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
20473
21515
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
21516
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
20474
21517
|
run_finished_callback=None,
|
|
20475
21518
|
):
|
|
20476
21519
|
self.root = root
|
|
@@ -20493,6 +21536,7 @@ class SessionManager:
|
|
|
20493
21536
|
MIN_AGENT_ROUNDS,
|
|
20494
21537
|
min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
|
|
20495
21538
|
)
|
|
21539
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
20496
21540
|
self.max_run_seconds = normalize_timeout_seconds(
|
|
20497
21541
|
max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
|
|
20498
21542
|
minimum=MIN_RUN_TIMEOUT_SECONDS,
|
|
@@ -20506,6 +21550,7 @@ class SessionManager:
|
|
|
20506
21550
|
self.arbiter_max_tokens = max(24, min(256, int(arbiter_max_tokens or ARBITER_DEFAULT_MAX_TOKENS)))
|
|
20507
21551
|
self.arbiter_temperature = max(0.0, min(1.0, float(arbiter_temperature if arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)))
|
|
20508
21552
|
self.execution_mode = normalize_execution_mode(execution_mode, default=EXECUTION_MODE_SYNC)
|
|
21553
|
+
self.single_advance_prompt_enhance = False
|
|
20509
21554
|
env_ok, env_tags, _ = probe_ollama_environment(ollama_base)
|
|
20510
21555
|
self.ollama_env_available = bool(env_ok)
|
|
20511
21556
|
self.ollama_env_tags: list[str] = list(env_tags)
|
|
@@ -20772,6 +21817,7 @@ class SessionManager:
|
|
|
20772
21817
|
min(1.0, float(self.arbiter_temperature if self.arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)),
|
|
20773
21818
|
)
|
|
20774
21819
|
sess.execution_mode = normalize_execution_mode(self.execution_mode, default=EXECUTION_MODE_SYNC)
|
|
21820
|
+
sess.single_advance_prompt_enhance = bool(self.single_advance_prompt_enhance)
|
|
20775
21821
|
sess._apply_active_profile()
|
|
20776
21822
|
sess.updated_at = now_ts()
|
|
20777
21823
|
sess._persist()
|
|
@@ -20839,6 +21885,7 @@ class SessionManager:
|
|
|
20839
21885
|
arbiter_max_tokens=self.arbiter_max_tokens,
|
|
20840
21886
|
arbiter_temperature=self.arbiter_temperature,
|
|
20841
21887
|
execution_mode=self.execution_mode,
|
|
21888
|
+
max_output_tokens=self.max_output_tokens,
|
|
20842
21889
|
ui_language=self.user_language,
|
|
20843
21890
|
js_lib_root=self.js_lib_root,
|
|
20844
21891
|
owner_user_id=self.user_id,
|
|
@@ -20879,6 +21926,7 @@ class SessionManager:
|
|
|
20879
21926
|
arbiter_max_tokens=self.arbiter_max_tokens,
|
|
20880
21927
|
arbiter_temperature=self.arbiter_temperature,
|
|
20881
21928
|
execution_mode=self.execution_mode,
|
|
21929
|
+
max_output_tokens=self.max_output_tokens,
|
|
20882
21930
|
ui_language=self.user_language,
|
|
20883
21931
|
js_lib_root=self.js_lib_root,
|
|
20884
21932
|
owner_user_id=self.user_id,
|
|
@@ -21291,7 +22339,7 @@ window.MathJax={
|
|
|
21291
22339
|
<button id="applyModelBtn" class="subtle">Apply Model</button>
|
|
21292
22340
|
<button id="importConfigBtn" class="subtle">Upload LLM.config.json</button>
|
|
21293
22341
|
<input id="configInput" type="file" accept=".json,application/json" style="display:none">
|
|
21294
|
-
<a id="downloadBtn" href="#"
|
|
22342
|
+
<a id="downloadBtn" href="#">Open Skills Studio</a>
|
|
21295
22343
|
</div>
|
|
21296
22344
|
</header>
|
|
21297
22345
|
<div class="status-cards" id="topStats"></div>
|
|
@@ -21320,7 +22368,15 @@ window.MathJax={
|
|
|
21320
22368
|
<button id="interruptBtn" class="subtle">Interrupt</button>
|
|
21321
22369
|
<button id="compactBtn" class="subtle">Compact</button>
|
|
21322
22370
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
21323
|
-
<
|
|
22371
|
+
<div class="export-dropdown" style="position:relative;display:inline-block">
|
|
22372
|
+
<button id="exportMenuBtn" class="subtle">Export ▾</button>
|
|
22373
|
+
<div id="exportMenu" style="display:none;position:absolute;bottom:100%;left:0;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.12);z-index:999;min-width:160px;padding:4px 0;margin-bottom:4px">
|
|
22374
|
+
<a id="downloadSessionBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export ZIP</a>
|
|
22375
|
+
<a id="exportMdBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Markdown</a>
|
|
22376
|
+
<a id="exportPdfBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export PDF</a>
|
|
22377
|
+
<a id="exportPngBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Image</a>
|
|
22378
|
+
</div>
|
|
22379
|
+
</div>
|
|
21324
22380
|
<div id="ctxLive" class="ctx-live" title="Remaining context budget">
|
|
21325
22381
|
<span class="ctx-live-dot"></span>
|
|
21326
22382
|
<span id="ctxLiveText" class="mono">ctx_left=-</span>
|
|
@@ -21394,6 +22450,7 @@ button,a{border:1px solid var(--line);padding:10px 14px;border-radius:12px;backg
|
|
|
21394
22450
|
button:hover,a:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(15,27,45,.08)}
|
|
21395
22451
|
#sendBtn,#newSessionBtn{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#fff;border:0}
|
|
21396
22452
|
.subtle{background:#f6f8fa}
|
|
22453
|
+
.export-item:hover{background:#f0f4f8}
|
|
21397
22454
|
.actions select{padding:10px 12px;border-radius:12px;border:1px solid var(--line);background:#fff;min-width:160px}
|
|
21398
22455
|
.think-switch{display:flex;align-items:center;gap:6px;border:1px solid var(--line);padding:8px 10px;border-radius:12px;background:#fff;font-weight:600}
|
|
21399
22456
|
.danger{color:var(--warn);border-color:#f3c0c0}
|
|
@@ -21508,7 +22565,7 @@ main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px)
|
|
|
21508
22565
|
.msg-md blockquote{margin:.5rem 0;padding:.4rem .6rem;border-left:3px solid #9db8e8;background:#eef4ff;border-radius:6px;color:#27446f}
|
|
21509
22566
|
.msg-md .md-inline-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.86em;padding:1px 5px;border-radius:5px;background:#e8eef8;border:1px solid #d3dded;color:#1e3a5f;white-space:pre}
|
|
21510
22567
|
.msg-md .md-code-lang{display:inline-block;margin:.3rem 0 0;padding:2px 8px;border:1px solid #cfd9ea;border-bottom:0;border-radius:8px 8px 0 0;background:#f3f7fd;color:#3b4f6d;font-size:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
21511
|
-
.msg-md .md-code{margin:0 0 .55rem;max-width:100%;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:0 8px 8px 8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.4;white-space:pre}
|
|
22568
|
+
.msg-md .md-code{margin:0 0 .55rem;max-width:100%;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:0 8px 8px 8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.4;white-space:pre;overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
21512
22569
|
.msg-md .md-table{margin:.5rem 0;border-collapse:collapse;max-width:100%;width:100%;display:block;overflow:auto;background:#fff}
|
|
21513
22570
|
.msg-md .md-table th,.msg-md .md-table td{border:1px solid #d7e0ec;padding:6px 8px;text-align:left;vertical-align:top;white-space:nowrap}
|
|
21514
22571
|
.msg-md .md-table th{background:#f5f8fc;font-weight:700}
|
|
@@ -21618,7 +22675,7 @@ h3{font-size:.96rem;margin:10px 0 6px}
|
|
|
21618
22675
|
|
|
21619
22676
|
APP_JS = """const S={sessions:[],activeId:null,snap:null,es:null,esId:'',skills:[],tools:[],providers:[],protocols:[],config:null,models:[],modelOptions:[],previewBySession:{},fileExplorerBySession:{},previewNonce:0,refreshTimer:null,refreshInFlight:false,pendingSnapshot:false,pendingFullSnapshot:false,scheduledFullSnapshot:false,sessionPollTimer:null,renderStateInFlight:false,lastRenderStatePullAt:0,lastFeedSig:'',lastBoardsSig:'',lastSessionsSig:'',lastVisibilityState:document.visibilityState||'visible',staticMode:false,frozen:false,bootRendered:false,panelHtml:{},follow:{chat:true,sessionList:false,todos:false,tasks:false,activity:true,commands:true,diffs:true,catalog:true,fileExplorer:false},lastEventSeq:0,lastDeltaTs:0,deltaGapCount:0,deltaWatchdogTimer:null,deltaWatchdogStalls:0,deltaWatchdogSeq:0,deltaRenderRaf:0,deltaRenderChat:false,deltaRenderBoards:false,deltaRenderSessions:false,mathObserver:null,mathRoot:null,mdWorker:null,mdWorkerUrl:'',mdReqSeq:0,mdPending:Object.create(null),diffCenterDisabled:Object.create(null),previewCenterDisabled:Object.create(null),diffCenteredDone:Object.create(null),previewCenteredDone:Object.create(null)};
|
|
21620
22677
|
const MD_CACHE=new Map();
|
|
21621
|
-
const MD_CACHE_MAX=
|
|
22678
|
+
const MD_CACHE_MAX=280;
|
|
21622
22679
|
const STATIC_UI=((new URLSearchParams(location.search)).get('static_ui')==='1');
|
|
21623
22680
|
const SNAPSHOT_DELAY_VISIBLE_MS=300;
|
|
21624
22681
|
const SNAPSHOT_DELAY_HIDDEN_MS=2400;
|
|
@@ -21633,30 +22690,30 @@ const CHAT_SCROLL_RENDER_THROTTLE_MS=70;
|
|
|
21633
22690
|
const CHAT_SCROLL_SYNC_DEBOUNCE_MS=260;
|
|
21634
22691
|
const CHAT_SCROLL_SETTLE_MS=620;
|
|
21635
22692
|
const CHAT_SCROLL_SETTLE_EPS_PX=1;
|
|
21636
|
-
const DELTA_MAX_FEED=
|
|
21637
|
-
const DELTA_MAX_MESSAGES=
|
|
21638
|
-
const DELTA_MAX_ACTIVITY=
|
|
21639
|
-
const DELTA_MAX_OPERATIONS=
|
|
22693
|
+
const DELTA_MAX_FEED=300;
|
|
22694
|
+
const DELTA_MAX_MESSAGES=300;
|
|
22695
|
+
const DELTA_MAX_ACTIVITY=80;
|
|
22696
|
+
const DELTA_MAX_OPERATIONS=160;
|
|
21640
22697
|
const DELTA_MAX_UPLOADS=40;
|
|
21641
22698
|
const DELTA_WATCHDOG_INTERVAL_MS=1800;
|
|
21642
22699
|
const DELTA_WATCHDOG_STALL_MS=9000;
|
|
21643
22700
|
const MARKDOWN_WORKER_MIN_CHARS=800;
|
|
21644
22701
|
const MARKDOWN_WORKER_MAX_PENDING=96;
|
|
21645
22702
|
const MARKDOWN_WORKER_REQ_TTL_MS=45000;
|
|
21646
|
-
const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:
|
|
22703
|
+
const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:600,poolByKind:Object.create(null),poolSize:0,poolMax:180};
|
|
21647
22704
|
const RENDER_EVT_TYPES=new Set(['render_frame','render_bridge']);
|
|
21648
|
-
const RENDER_QUEUE_MAX=
|
|
22705
|
+
const RENDER_QUEUE_MAX=80;
|
|
21649
22706
|
const RENDER_META_MIN_INTERVAL_MS=180;
|
|
21650
22707
|
const RENDER={queue:[],raf:0,canvas:null,ctx:null,lastSeq:0,lastPaintAt:0,lastMetaAt:0,lastSummary:'',hideTimer:0,imgTicket:0};
|
|
21651
22708
|
const CODE_PREVIEW_VIRT_THRESHOLD=1800;
|
|
21652
22709
|
const CODE_PREVIEW_VIRT_EST_ROW_PX=24;
|
|
21653
22710
|
const CODE_PREVIEW_VIRT_OVERSCAN=160;
|
|
21654
|
-
const CODE_PREVIEW_EXTS=new Set(['.py','.pyi','.js','.mjs','.cjs','.ts','.tsx','.jsx','.java','.c','.cc','.cpp','.cxx','.h','.hh','.hpp','.hxx','.go','.rs','.rb','.php','.swift','.kt','.kts','.scala','.sh','.bash','.zsh','.fish','.ps1','.bat','.sql','.json','.jsonc','.yaml','.yml','.toml','.ini','.cfg','.conf','.xml','.xsd','.xsl','.cs','.m','.mm','.r','.pl','.lua','.dart','.vue','.svelte','.gradle','.properties']);
|
|
22711
|
+
const CODE_PREVIEW_EXTS=new Set(['.py','.pyi','.js','.mjs','.cjs','.ts','.tsx','.jsx','.java','.c','.cc','.cpp','.cxx','.h','.hh','.hpp','.hxx','.go','.rs','.rb','.php','.swift','.kt','.kts','.scala','.sh','.bash','.zsh','.fish','.ps1','.bat','.sql','.json','.jsonc','.yaml','.yml','.toml','.ini','.cfg','.conf','.xml','.xsd','.xsl','.cs','.m','.mm','.r','.pl','.lua','.dart','.vue','.svelte','.gradle','.properties','.f','.f90','.f95','.f03','.f08','.for','.fpp','.hs','.lhs','.erl','.hrl','.ex','.exs','.ml','.mli','.vhd','.vhdl','.v','.sv','.asm','.s','.proto','.tf','.tfvars','.prisma','.graphql','.gql','.zig','.nim','.jl','.cr','.d','.clj','.cljs','.cljc','.lisp','.cl','.el','.rkt','.pas','.pp','.wgsl','.glsl','.hlsl','.groovy','.cmake','.dockerfile']);
|
|
21655
22712
|
const CODE_PREVIEW_FILENAMES=new Set(['dockerfile','makefile','cmakelists.txt','justfile','gemfile','rakefile','pipfile','requirements.txt']);
|
|
21656
|
-
const CODE_LANG_BY_EXT={'.py':'python','.pyi':'python','.js':'javascript','.mjs':'javascript','.cjs':'javascript','.ts':'typescript','.tsx':'typescript','.jsx':'javascript','.java':'java','.c':'c','.cc':'cpp','.cpp':'cpp','.cxx':'cpp','.h':'c','.hh':'cpp','.hpp':'cpp','.hxx':'cpp','.go':'go','.rs':'rust','.rb':'ruby','.php':'php','.swift':'swift','.kt':'kotlin','.kts':'kotlin','.scala':'scala','.sh':'shell','.bash':'shell','.zsh':'shell','.fish':'shell','.ps1':'shell','.bat':'shell','.sql':'sql','.json':'json','.jsonc':'json','.yaml':'yaml','.yml':'yaml','.toml':'toml','.ini':'ini','.cfg':'ini','.conf':'ini','.xml':'xml','.xsd':'xml','.xsl':'xml','.cs':'csharp','.m':'objectivec','.mm':'objectivec','.r':'r','.pl':'perl','.lua':'lua','.dart':'dart','.vue':'javascript','.svelte':'javascript','.gradle':'groovy','.properties':'ini'};
|
|
22713
|
+
const CODE_LANG_BY_EXT={'.py':'python','.pyi':'python','.js':'javascript','.mjs':'javascript','.cjs':'javascript','.ts':'typescript','.tsx':'typescript','.jsx':'javascript','.java':'java','.c':'c','.cc':'cpp','.cpp':'cpp','.cxx':'cpp','.h':'c','.hh':'cpp','.hpp':'cpp','.hxx':'cpp','.go':'go','.rs':'rust','.rb':'ruby','.php':'php','.swift':'swift','.kt':'kotlin','.kts':'kotlin','.scala':'scala','.sh':'shell','.bash':'shell','.zsh':'shell','.fish':'shell','.ps1':'shell','.bat':'shell','.sql':'sql','.json':'json','.jsonc':'json','.yaml':'yaml','.yml':'yaml','.toml':'toml','.ini':'ini','.cfg':'ini','.conf':'ini','.xml':'xml','.xsd':'xml','.xsl':'xml','.cs':'csharp','.m':'objectivec','.mm':'objectivec','.r':'r','.pl':'perl','.lua':'lua','.dart':'dart','.vue':'javascript','.svelte':'javascript','.gradle':'groovy','.properties':'ini','.f':'fortran','.f90':'fortran','.f95':'fortran','.f03':'fortran','.f08':'fortran','.for':'fortran','.fpp':'fortran','.hs':'haskell','.lhs':'haskell','.erl':'erlang','.hrl':'erlang','.ex':'elixir','.exs':'elixir','.ml':'ocaml','.mli':'ocaml','.vhd':'vhdl','.vhdl':'vhdl','.v':'verilog','.sv':'verilog','.asm':'asm','.s':'asm','.proto':'protobuf','.tf':'hcl','.tfvars':'hcl','.zig':'zig','.nim':'nim','.jl':'julia','.cr':'crystal','.d':'dlang','.clj':'clojure','.cljs':'clojure','.cljc':'clojure','.lisp':'lisp','.cl':'lisp','.el':'lisp','.rkt':'racket','.pas':'pascal','.pp':'pascal','.wgsl':'wgsl','.glsl':'glsl','.hlsl':'hlsl','.groovy':'groovy','.cmake':'cmake','.dockerfile':'shell','.prisma':'prisma','.graphql':'graphql','.gql':'graphql'};
|
|
21657
22714
|
const CODE_LANG_BY_NAME={'dockerfile':'shell','makefile':'makefile','cmakelists.txt':'cmake','justfile':'makefile','gemfile':'ruby','rakefile':'ruby','pipfile':'ini','requirements.txt':'ini'};
|
|
21658
22715
|
const CODE_LITERAL_WORDS=new Set(['true','false','null','undefined','none','nil']);
|
|
21659
|
-
const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case','break','continue','return','function','class','import','export','from','try','catch','finally','throw','new','const','let','var','public','private','protected','static','async','await']),python:new Set(['def','class','if','elif','else','for','while','try','except','finally','raise','return','yield','import','from','as','with','pass','break','continue','lambda','global','nonlocal','assert','del','in','is','not','and','or','async','await']),javascript:new Set(['function','class','if','else','for','while','do','switch','case','break','continue','return','try','catch','finally','throw','new','this','const','let','var','import','export','from','default','extends','super','async','await','typeof','instanceof','in','of']),typescript:new Set(['interface','type','enum','implements','readonly','namespace','declare','keyof','infer','satisfies','as','extends','public','private','protected','abstract','override','function','class','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','const','let','var','import','export','from','async','await']),java:new Set(['class','interface','enum','extends','implements','public','private','protected','static','final','abstract','volatile','synchronized','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','new','package','import','instanceof','this','super','void']),c:new Set(['if','else','for','while','switch','case','break','continue','return','typedef','struct','union','enum','static','const','volatile','extern','inline','sizeof','#include','#define']),cpp:new Set(['if','else','for','while','switch','case','break','continue','return','class','struct','namespace','template','typename','public','private','protected','virtual','override','const','static','auto','constexpr','using','new','delete','this','throw','try','catch','#include','#define']),go:new Set(['package','import','func','type','struct','interface','map','chan','go','defer','select','if','else','for','switch','case','break','continue','return','fallthrough','range','const','var']),rust:new Set(['fn','let','mut','impl','trait','struct','enum','match','if','else','for','while','loop','break','continue','return','pub','use','mod','crate','self','super','where','async','await','move']),ruby:new Set(['def','class','module','if','elsif','else','unless','case','when','for','while','until','begin','rescue','ensure','return','yield','super','self','require','include','extend','end']),php:new Set(['function','class','interface','trait','public','private','protected','static','if','elseif','else','for','foreach','while','switch','case','break','continue','return','try','catch','finally','throw','namespace','use','new']),swift:new Set(['func','class','struct','enum','protocol','extension','if','else','guard','for','while','switch','case','break','continue','return','defer','do','catch','throw','try','import','let','var']),kotlin:new Set(['fun','class','interface','object','data','sealed','enum','if','else','when','for','while','do','break','continue','return','try','catch','throw','import','package','val','var','companion']),scala:new Set(['def','class','trait','object','case','if','else','for','while','match','break','continue','return','try','catch','throw','import','package','val','var','extends','with']),shell:new Set(['if','then','else','fi','for','do','done','while','case','esac','function','return','break','continue','export','local','readonly','in']),sql:new Set(['select','from','where','group','by','order','insert','into','values','update','set','delete','join','left','right','inner','outer','on','create','alter','drop','table','view','index','and','or','not','as','limit']),json:new Set([]),yaml:new Set([]),toml:new Set([]),ini:new Set([]),xml:new Set([]),csharp:new Set(['namespace','class','interface','struct','enum','public','private','protected','internal','static','readonly','const','if','else','for','foreach','while','switch','case','break','continue','return','using','new','this','base','async','await']),objectivec:new Set(['@interface','@implementation','@property','@synthesize','@end','if','else','for','while','switch','case','break','continue','return','#import']),r:new Set(['if','else','for','while','repeat','break','next','function','return','library']),perl:new Set(['if','elsif','else','for','foreach','while','last','next','sub','my','our','use','package','return']),lua:new Set(['if','then','else','elseif','end','for','while','repeat','until','break','function','local','return']),dart:new Set(['class','enum','extension','if','else','for','while','switch','case','break','continue','return','import','library','part','new','const','final','var','async','await']),groovy:new Set(['class','interface','trait','if','else','for','while','switch','case','break','continue','return','def','import','package','new']),makefile:new Set(['include','ifeq','ifneq','ifdef','ifndef','else','endif']),cmake:new Set(['if','else','elseif','endif','foreach','endforeach','while','endwhile','function','endfunction','macro','endmacro','set','add_executable','add_library'])};
|
|
22716
|
+
const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case','break','continue','return','function','class','import','export','from','try','catch','finally','throw','new','const','let','var','public','private','protected','static','async','await']),python:new Set(['def','class','if','elif','else','for','while','try','except','finally','raise','return','yield','import','from','as','with','pass','break','continue','lambda','global','nonlocal','assert','del','in','is','not','and','or','async','await']),javascript:new Set(['function','class','if','else','for','while','do','switch','case','break','continue','return','try','catch','finally','throw','new','this','const','let','var','import','export','from','default','extends','super','async','await','typeof','instanceof','in','of']),typescript:new Set(['interface','type','enum','implements','readonly','namespace','declare','keyof','infer','satisfies','as','extends','public','private','protected','abstract','override','function','class','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','const','let','var','import','export','from','async','await']),java:new Set(['class','interface','enum','extends','implements','public','private','protected','static','final','abstract','volatile','synchronized','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','new','package','import','instanceof','this','super','void']),c:new Set(['if','else','for','while','switch','case','break','continue','return','typedef','struct','union','enum','static','const','volatile','extern','inline','sizeof','#include','#define']),cpp:new Set(['if','else','for','while','switch','case','break','continue','return','class','struct','namespace','template','typename','public','private','protected','virtual','override','const','static','auto','constexpr','using','new','delete','this','throw','try','catch','#include','#define']),go:new Set(['package','import','func','type','struct','interface','map','chan','go','defer','select','if','else','for','switch','case','break','continue','return','fallthrough','range','const','var']),rust:new Set(['fn','let','mut','impl','trait','struct','enum','match','if','else','for','while','loop','break','continue','return','pub','use','mod','crate','self','super','where','async','await','move']),ruby:new Set(['def','class','module','if','elsif','else','unless','case','when','for','while','until','begin','rescue','ensure','return','yield','super','self','require','include','extend','end']),php:new Set(['function','class','interface','trait','public','private','protected','static','if','elseif','else','for','foreach','while','switch','case','break','continue','return','try','catch','finally','throw','namespace','use','new']),swift:new Set(['func','class','struct','enum','protocol','extension','if','else','guard','for','while','switch','case','break','continue','return','defer','do','catch','throw','try','import','let','var']),kotlin:new Set(['fun','class','interface','object','data','sealed','enum','if','else','when','for','while','do','break','continue','return','try','catch','throw','import','package','val','var','companion']),scala:new Set(['def','class','trait','object','case','if','else','for','while','match','break','continue','return','try','catch','throw','import','package','val','var','extends','with']),shell:new Set(['if','then','else','fi','for','do','done','while','case','esac','function','return','break','continue','export','local','readonly','in']),sql:new Set(['select','from','where','group','by','order','insert','into','values','update','set','delete','join','left','right','inner','outer','on','create','alter','drop','table','view','index','and','or','not','as','limit']),json:new Set([]),yaml:new Set([]),toml:new Set([]),ini:new Set([]),xml:new Set([]),csharp:new Set(['namespace','class','interface','struct','enum','public','private','protected','internal','static','readonly','const','if','else','for','foreach','while','switch','case','break','continue','return','using','new','this','base','async','await']),objectivec:new Set(['@interface','@implementation','@property','@synthesize','@end','if','else','for','while','switch','case','break','continue','return','#import']),r:new Set(['if','else','for','while','repeat','break','next','function','return','library']),perl:new Set(['if','elsif','else','for','foreach','while','last','next','sub','my','our','use','package','return']),lua:new Set(['if','then','else','elseif','end','for','while','repeat','until','break','function','local','return']),dart:new Set(['class','enum','extension','if','else','for','while','switch','case','break','continue','return','import','library','part','new','const','final','var','async','await']),groovy:new Set(['class','interface','trait','if','else','for','while','switch','case','break','continue','return','def','import','package','new']),makefile:new Set(['include','ifeq','ifneq','ifdef','ifndef','else','endif']),cmake:new Set(['if','else','elseif','endif','foreach','endforeach','while','endwhile','function','endfunction','macro','endmacro','set','add_executable','add_library']),fortran:new Set(['program','module','subroutine','function','end','use','implicit','none','integer','real','character','logical','complex','dimension','allocatable','intent','in','out','inout','do','if','then','else','elseif','endif','call','return','write','read','format','type','class','interface','contains','allocate','deallocate']),haskell:new Set(['module','where','import','qualified','as','hiding','data','type','newtype','class','instance','deriving','if','then','else','case','of','let','in','do','return','where','infixl','infixr','infix','forall','default']),erlang:new Set(['module','export','import','if','case','of','end','receive','after','when','fun','try','catch','throw','begin','andalso','orelse','not','band','bor','bxor','bnot','bsl','bsr']),elixir:new Set(['def','defp','defmodule','defmacro','defstruct','defprotocol','defimpl','if','else','unless','case','cond','do','end','fn','when','with','for','raise','rescue','import','use','alias','require']),ocaml:new Set(['let','in','if','then','else','match','with','fun','function','type','module','struct','sig','end','open','val','rec','and','or','not','begin','do','done','for','while','to','downto','mutable','ref']),vhdl:new Set(['library','use','entity','architecture','is','of','begin','end','signal','variable','constant','port','in','out','inout','process','if','then','else','elsif','case','when','for','generate','component','generic','map']),verilog:new Set(['module','endmodule','input','output','inout','wire','reg','assign','always','begin','end','if','else','case','endcase','for','while','parameter','localparam','initial','posedge','negedge','task','function']),asm:new Set([]),protobuf:new Set(['syntax','package','import','option','message','enum','service','rpc','returns','repeated','optional','required','map','oneof','reserved','extend']),hcl:new Set(['resource','data','variable','output','locals','module','provider','terraform','backend','required_providers','for_each','count','depends_on','lifecycle','dynamic','content','block']),zig:new Set(['const','var','fn','pub','return','if','else','for','while','break','continue','switch','struct','enum','union','error','defer','errdefer','try','catch','import','comptime','inline','test','unreachable']),nim:new Set(['proc','func','method','type','var','let','const','if','elif','else','case','of','for','while','break','continue','return','import','include','from','object','ref','ptr','template','macro','iterator','yield','discard']),julia:new Set(['function','end','if','elseif','else','for','while','break','continue','return','module','using','import','export','struct','mutable','abstract','type','const','let','do','begin','try','catch','finally','throw','macro','quote']),crystal:new Set(['def','class','module','struct','enum','if','elsif','else','unless','case','when','while','until','for','do','end','return','yield','begin','rescue','ensure','require','include','extend','abstract','private','protected']),dlang:new Set(['module','import','class','struct','enum','interface','if','else','for','foreach','while','do','switch','case','default','break','continue','return','void','auto','const','immutable','static','public','private','protected','override','template','mixin','alias']),clojure:new Set(['def','defn','defmacro','fn','let','if','cond','case','do','loop','recur','for','doseq','when','when-not','ns','require','use','import','try','catch','throw','finally']),lisp:new Set(['defun','defmacro','defvar','defparameter','defconstant','let','let*','if','cond','case','when','unless','lambda','progn','loop','do','dolist','dotimes','setq','setf','funcall','apply','require','provide']),racket:new Set(['define','lambda','let','let*','letrec','if','cond','case','when','unless','begin','do','for','for/list','for/hash','match','struct','class','require','provide','module','import','export']),pascal:new Set(['program','unit','uses','interface','implementation','begin','end','var','const','type','procedure','function','if','then','else','for','to','downto','while','repeat','until','case','of','with','record','class','array','set','nil']),wgsl:new Set(['fn','var','let','const','struct','if','else','for','while','loop','break','continue','return','switch','case','default','override','enable','type','alias','discard','continuing','fallthrough']),glsl:new Set(['void','float','int','bool','vec2','vec3','vec4','mat2','mat3','mat4','sampler2D','uniform','varying','attribute','in','out','inout','if','else','for','while','do','break','continue','return','struct','const','precision','highp','mediump','lowp']),hlsl:new Set(['float','float2','float3','float4','int','bool','void','struct','cbuffer','Texture2D','SamplerState','if','else','for','while','do','break','continue','return','in','out','inout','uniform','static','const','register','semantic']),prisma:new Set(['model','enum','datasource','generator','type','relation','default','unique','id','map','index','ignore','updatedAt']),graphql:new Set(['type','query','mutation','subscription','input','enum','interface','union','scalar','schema','fragment','on','directive','extend','implements'])};
|
|
21660
22717
|
S.staticMode=STATIC_UI;
|
|
21661
22718
|
const COMPACT_AUTO_REFRESH_COUNT=3;
|
|
21662
22719
|
const COMPACT_AUTO_REFRESH_INTERVAL_MS=260;
|
|
@@ -21820,18 +22877,18 @@ function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText'
|
|
|
21820
22877
|
function showCompactToast(text){let el=document.querySelector('.compact-toast');if(!el){el=document.createElement('div');el.className='compact-toast';document.body.appendChild(el)}el.textContent=text;el.classList.add('show');if(el._t)clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove('show'),2800)}
|
|
21821
22878
|
function parseCompactReason(data){const direct=String(data?.reason||'').trim();if(direct)return direct;const s=String(data?.summary||'');const m=s.match(/context compacted \\(([^)]*)\\)/);return m?String(m[1]||'').trim():''}
|
|
21822
22879
|
function isRenderRuntimeEventType(evtType){return RENDER_EVT_TYPES.has(String(evtType||''))}
|
|
21823
|
-
function pullRenderState(id,force=false){const sid=String(id||S.activeId||'').trim();if(!sid||S.renderStateInFlight)return;const now=Date.now();if(!force&&now-Number(S.lastRenderStatePullAt||0)<1200)return;S.lastRenderStatePullAt=now;S.renderStateInFlight=true;api('/api/sessions/'+sid+'/render-state').then(st=>{if(!st||typeof st!=='object')return;const frame=st?.frame;if(frame&&typeof frame==='object'){_renderBridgeEnqueue(frame);return}const seq=Number(st?.seq||0);if(seq<=0)return;const kind=String(st?.last_kind||st?.latest?.kind||'generic');const latest=st?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeShow();_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(
|
|
22880
|
+
function pullRenderState(id,force=false){const sid=String(id||S.activeId||'').trim();if(!sid||S.renderStateInFlight)return;const now=Date.now();if(!force&&now-Number(S.lastRenderStatePullAt||0)<1200)return;S.lastRenderStatePullAt=now;S.renderStateInFlight=true;api('/api/sessions/'+sid+'/render-state').then(st=>{if(!st||typeof st!=='object')return;const frame=st?.frame;if(frame&&typeof frame==='object'){_renderBridgeEnqueue(frame);return}const seq=Number(st?.seq||0);if(seq<=0)return;const kind=String(st?.last_kind||st?.latest?.kind||'generic');const latest=st?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeShow();_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(30000)}).catch(()=>{}).finally(()=>{S.renderStateInFlight=false})}
|
|
21824
22881
|
function _renderBridgeShow(){const wrap=E('renderBridge');if(!wrap)return null;wrap.classList.remove('hidden');if(RENDER.hideTimer){clearTimeout(RENDER.hideTimer);RENDER.hideTimer=0}return wrap}
|
|
21825
|
-
function _renderBridgeHideLater(ms=
|
|
22882
|
+
function _renderBridgeHideLater(ms=30000){if(RENDER.hideTimer){clearTimeout(RENDER.hideTimer);RENDER.hideTimer=0}RENDER.hideTimer=setTimeout(()=>{const wrap=E('renderBridge');if(wrap)wrap.classList.add('hidden')},Math.max(4000,Number(ms)||30000))}
|
|
21826
22883
|
function _renderBridgeEnsureCanvas(){const canvas=E('renderCanvas');if(!canvas)return null;const wrap=_renderBridgeShow();if(!wrap)return null;const ctx=(canvas.getContext&&canvas.getContext('2d'))?canvas.getContext('2d'):null;if(!ctx)return null;const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1));const rect=canvas.getBoundingClientRect();const w=Math.max(260,Math.floor(rect.width||canvas.clientWidth||canvas.width||960));const h=Math.max(120,Math.floor(rect.height||canvas.clientHeight||canvas.height||220));const bw=Math.max(260,Math.floor(w*dpr));const bh=Math.max(120,Math.floor(h*dpr));if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;ctx.setTransform(1,0,0,1,0,0);ctx.scale(dpr,dpr)}RENDER.canvas=canvas;RENDER.ctx=ctx;return{canvas,ctx,w,h}}
|
|
21827
22884
|
function _renderBridgeSafeNum(v,def=0,min=-1e6,max=1e6){const n=Number(v);if(!Number.isFinite(n))return def;return Math.max(min,Math.min(max,n))}
|
|
21828
22885
|
function _renderBridgeColor(raw,fallback='#1f6feb'){const s=String(raw||'').trim();if(/^#[0-9a-fA-F]{3,8}$/.test(s))return s;if(/^rgba?\\(([^)]+)\\)$/.test(s))return s;if(/^hsla?\\(([^)]+)\\)$/.test(s))return s;return fallback}
|
|
21829
22886
|
function _renderBridgeUpdateMeta(text,force=false){const meta=E('renderMeta');if(!meta)return;const now=Date.now();if(!force&&now-Number(RENDER.lastMetaAt||0)<RENDER_META_MIN_INTERVAL_MS)return;const txt=String(text||'').trim();if(!txt)return;RENDER.lastMetaAt=now;RENDER.lastSummary=txt;meta.textContent=txt}
|
|
21830
22887
|
function _renderBridgeDrawImage(frame,ctx,w,h){const b64=String(frame?.image_b64||'').trim();if(!b64)return;const mime=String(frame?.mime||'image/png').trim()||'image/png';const src=`data:${mime};base64,${b64}`;const ticket=Number(RENDER.imgTicket||0)+1;RENDER.imgTicket=ticket;const img=new Image();img.onload=()=>{if(ticket!==Number(RENDER.imgTicket||0))return;ctx.drawImage(img,0,0,w,h)};img.onerror=()=>{};img.src=src}
|
|
21831
|
-
function _renderBridgeDrawFrame(frame){const ready=_renderBridgeEnsureCanvas();if(!ready)return;const {ctx,w,h}=ready;const srcW=Math.max(1,_renderBridgeSafeNum(frame?.width,0,0,8192));const srcH=Math.max(1,_renderBridgeSafeNum(frame?.height,0,0,8192));const sx=(srcW>0)?(w/srcW):1;const sy=(srcH>0)?(h/srcH):1;const clear=!!frame?.clear||!RENDER.lastPaintAt;if(clear){ctx.clearRect(0,0,w,h)}const bg=String(frame?.bg||'').trim();if(bg){ctx.fillStyle=_renderBridgeColor(bg,'#ffffff');ctx.fillRect(0,0,w,h)}if(String(frame?.image_b64||'').trim()){_renderBridgeDrawImage(frame,ctx,w,h)}const lines=Array.isArray(frame?.lines)?frame.lines:[];if(lines.length){for(const line of lines){const pts=Array.isArray(line?.points)?line.points:[];if(pts.length<2)continue;ctx.beginPath();const p0=pts[0];const x0=_renderBridgeSafeNum(Array.isArray(p0)?p0[0]:0,0)*sx;const y0=_renderBridgeSafeNum(Array.isArray(p0)?p0[1]:0,0)*sy;ctx.moveTo(x0,y0);for(let i=1;i<pts.length;i++){const p=pts[i];ctx.lineTo(_renderBridgeSafeNum(Array.isArray(p)?p[0]:0,0)*sx,_renderBridgeSafeNum(Array.isArray(p)?p[1]:0,0)*sy)}ctx.strokeStyle=_renderBridgeColor(line?.color,'#1f6feb');ctx.globalAlpha=_renderBridgeSafeNum(line?.alpha,1,0,1);ctx.lineWidth=_renderBridgeSafeNum(line?.width,1.6,0.1,60);ctx.stroke();ctx.globalAlpha=1}}const points=Array.isArray(frame?.points)?frame.points:[];if(points.length){for(const p of points){ctx.beginPath();ctx.arc(_renderBridgeSafeNum(p?.x,0)*sx,_renderBridgeSafeNum(p?.y,0)*sy,_renderBridgeSafeNum(p?.size,1.6,0.1,40),0,Math.PI*2);ctx.fillStyle=_renderBridgeColor(p?.color,'#1a7f64');ctx.globalAlpha=_renderBridgeSafeNum(p?.alpha,1,0,1);ctx.fill();ctx.globalAlpha=1}}const txt=String(frame?.text||'').trim();if(txt){ctx.fillStyle='#26364d';ctx.font='12px ui-monospace, SFMono-Regular, Menlo, monospace';ctx.fillText(txt.slice(0,240),8,18)}RENDER.lastPaintAt=Date.now();RENDER.lastSeq=Math.max(Number(RENDER.lastSeq||0),Number(frame?.seq||0));const lineCount=Array.isArray(frame?.lines)?frame.lines.length:0;const pointCount=Array.isArray(frame?.points)?frame.points.length:0;const kind=String(frame?.kind||'generic');_renderBridgeUpdateMeta(`render seq=${RENDER.lastSeq} · kind=${kind} · lines=${lineCount} · points=${pointCount}`,true);_renderBridgeHideLater(
|
|
22888
|
+
function _renderBridgeDrawFrame(frame){const ready=_renderBridgeEnsureCanvas();if(!ready)return;const {ctx,w,h}=ready;const srcW=Math.max(1,_renderBridgeSafeNum(frame?.width,0,0,8192));const srcH=Math.max(1,_renderBridgeSafeNum(frame?.height,0,0,8192));const sx=(srcW>0)?(w/srcW):1;const sy=(srcH>0)?(h/srcH):1;const clear=!!frame?.clear||!RENDER.lastPaintAt;if(clear){ctx.clearRect(0,0,w,h)}const bg=String(frame?.bg||'').trim();if(bg){ctx.fillStyle=_renderBridgeColor(bg,'#ffffff');ctx.fillRect(0,0,w,h)}if(String(frame?.image_b64||'').trim()){_renderBridgeDrawImage(frame,ctx,w,h)}const lines=Array.isArray(frame?.lines)?frame.lines:[];if(lines.length){for(const line of lines){const pts=Array.isArray(line?.points)?line.points:[];if(pts.length<2)continue;ctx.beginPath();const p0=pts[0];const x0=_renderBridgeSafeNum(Array.isArray(p0)?p0[0]:0,0)*sx;const y0=_renderBridgeSafeNum(Array.isArray(p0)?p0[1]:0,0)*sy;ctx.moveTo(x0,y0);for(let i=1;i<pts.length;i++){const p=pts[i];ctx.lineTo(_renderBridgeSafeNum(Array.isArray(p)?p[0]:0,0)*sx,_renderBridgeSafeNum(Array.isArray(p)?p[1]:0,0)*sy)}ctx.strokeStyle=_renderBridgeColor(line?.color,'#1f6feb');ctx.globalAlpha=_renderBridgeSafeNum(line?.alpha,1,0,1);ctx.lineWidth=_renderBridgeSafeNum(line?.width,1.6,0.1,60);ctx.stroke();ctx.globalAlpha=1}}const points=Array.isArray(frame?.points)?frame.points:[];if(points.length){for(const p of points){ctx.beginPath();ctx.arc(_renderBridgeSafeNum(p?.x,0)*sx,_renderBridgeSafeNum(p?.y,0)*sy,_renderBridgeSafeNum(p?.size,1.6,0.1,40),0,Math.PI*2);ctx.fillStyle=_renderBridgeColor(p?.color,'#1a7f64');ctx.globalAlpha=_renderBridgeSafeNum(p?.alpha,1,0,1);ctx.fill();ctx.globalAlpha=1}}const txt=String(frame?.text||'').trim();if(txt){ctx.fillStyle='#26364d';ctx.font='12px ui-monospace, SFMono-Regular, Menlo, monospace';ctx.fillText(txt.slice(0,240),8,18)}RENDER.lastPaintAt=Date.now();RENDER.lastSeq=Math.max(Number(RENDER.lastSeq||0),Number(frame?.seq||0));const lineCount=Array.isArray(frame?.lines)?frame.lines.length:0;const pointCount=Array.isArray(frame?.points)?frame.points.length:0;const kind=String(frame?.kind||'generic');_renderBridgeUpdateMeta(`render seq=${RENDER.lastSeq} · kind=${kind} · lines=${lineCount} · points=${pointCount}`,true);_renderBridgeHideLater(30000)}
|
|
21832
22889
|
function _renderBridgeDrain(){RENDER.raf=0;if(!Array.isArray(RENDER.queue)||!RENDER.queue.length)return;const latest=RENDER.queue[RENDER.queue.length-1]||{};RENDER.queue.length=0;_renderBridgeDrawFrame(latest);if(RENDER.queue.length){RENDER.raf=requestAnimationFrame(_renderBridgeDrain)}}
|
|
21833
22890
|
function _renderBridgeEnqueue(frame){if(!frame||typeof frame!=='object')return;_renderBridgeShow();if(!Array.isArray(RENDER.queue))RENDER.queue=[];RENDER.queue.push(frame);if(RENDER.queue.length>RENDER_QUEUE_MAX){RENDER.queue=RENDER.queue.slice(-Math.floor(RENDER_QUEUE_MAX*0.6))}const summary=String(frame?.summary||'').trim();if(summary){_renderBridgeUpdateMeta(summary,false)}if(!RENDER.raf){RENDER.raf=requestAnimationFrame(_renderBridgeDrain)}}
|
|
21834
|
-
function _renderBridgeSyncFromSnapshot(snap){const rb=snap?.render_bridge||null;if(!rb||typeof rb!=='object')return;const seq=Number(rb?.seq||0);if(seq<=0)return;_renderBridgeShow();const kind=String(rb?.last_kind||rb?.latest?.kind||'generic');const latest=rb?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(
|
|
22891
|
+
function _renderBridgeSyncFromSnapshot(snap){const rb=snap?.render_bridge||null;if(!rb||typeof rb!=='object')return;const seq=Number(rb?.seq||0);if(seq<=0)return;_renderBridgeShow();const kind=String(rb?.last_kind||rb?.latest?.kind||'generic');const latest=rb?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(30000)}
|
|
21835
22892
|
function _deltaEnsureSnapshot(){if(!S.snap||typeof S.snap!=='object')return false;if(!Array.isArray(S.snap.messages))S.snap.messages=[];if(!Array.isArray(S.snap.conversation_feed))S.snap.conversation_feed=[];if(!Array.isArray(S.snap.activity))S.snap.activity=[];if(!Array.isArray(S.snap.operations))S.snap.operations=[];if(!Array.isArray(S.snap.uploads))S.snap.uploads=[];if(!Array.isArray(S.snap.todos))S.snap.todos=[];if(!Array.isArray(S.snap.tasks))S.snap.tasks=[];return true}
|
|
21836
22893
|
function _deltaPushLimited(arr,row,maxCount){if(!Array.isArray(arr))return;if(!row||typeof row!=='object')return;arr.push(row);const maxN=Math.max(20,Number(maxCount)||120);if(arr.length>maxN){arr.splice(0,arr.length-maxN)}}
|
|
21837
22894
|
function _deltaAdoptAgentRole(data){if(!_deltaEnsureSnapshot())return'';const role=_chatVirtAgentRoleKey(data?.agent_role);if(!role)return'';S.snap.agent_active_role=role;return role}
|
|
@@ -21858,6 +22915,7 @@ function _deltaScheduleRender(flags={}){
|
|
|
21858
22915
|
if(S.deltaRenderRaf)return;
|
|
21859
22916
|
S.deltaRenderRaf=requestAnimationFrame(()=>{
|
|
21860
22917
|
S.deltaRenderRaf=0;
|
|
22918
|
+
if(S.refreshInFlight)return;
|
|
21861
22919
|
const needChat=!!S.deltaRenderChat;
|
|
21862
22920
|
const needBoards=!!S.deltaRenderBoards;
|
|
21863
22921
|
const needSessions=!!S.deltaRenderSessions;
|
|
@@ -22016,8 +23074,8 @@ function onRuntimeEvent(evt){
|
|
|
22016
23074
|
if(seqState.gap)return{handled:true,needsSnapshot:true};
|
|
22017
23075
|
const typ=String(evt.type||'');
|
|
22018
23076
|
if(typ==='render_frame'){_renderBridgeEnqueue(evt.data||{});return{handled:true,needsSnapshot:false}}
|
|
22019
|
-
if(typ==='render_bridge'){const d=evt.data||{};const summary=String(d?.summary||'').trim();if(summary){_renderBridgeShow();_renderBridgeUpdateMeta(summary,true);_renderBridgeHideLater(
|
|
22020
|
-
if(typ==='compact'){scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);const reason=parseCompactReason(evt.data||{});if(reason==='auto'||reason.startsWith('truncation-rescue')){const pct=Number(evt.data?.context_left_percent_before);const left=Number(evt.data?.context_left_before);const limit=Number(evt.data?.context_limit_before);const pctTxt=Number.isFinite(pct)?pct.toFixed(1):'-';const leftTxt=Number.isFinite(left)&&Number.isFinite(limit)?`${left}/${limit}`:'-';showCompactToast(`${t('compact_auto')}:${pctTxt}% left (${leftTxt}) · delta-sync`)}_deltaAppendActivity(typ,evt.data||{},Number(evt?.ts||Date.now()/1000));
|
|
23077
|
+
if(typ==='render_bridge'){const d=evt.data||{};const summary=String(d?.summary||'').trim();if(summary){_renderBridgeShow();_renderBridgeUpdateMeta(summary,true);_renderBridgeHideLater(30000)}return{handled:true,needsSnapshot:false}}
|
|
23078
|
+
if(typ==='compact'){scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);const reason=parseCompactReason(evt.data||{});if(reason==='auto'||reason.startsWith('truncation-rescue')){const pct=Number(evt.data?.context_left_percent_before);const left=Number(evt.data?.context_left_before);const limit=Number(evt.data?.context_limit_before);const pctTxt=Number.isFinite(pct)?pct.toFixed(1):'-';const leftTxt=Number.isFinite(left)&&Number.isFinite(limit)?`${left}/${limit}`:'-';showCompactToast(`${t('compact_auto')}:${pctTxt}% left (${leftTxt}) · delta-sync`)}_deltaAppendActivity(typ,evt.data||{},Number(evt?.ts||Date.now()/1000));return{handled:true,needsSnapshot:false}}
|
|
22021
23079
|
return _deltaApplyRuntimeEvent(evt);
|
|
22022
23080
|
}
|
|
22023
23081
|
function _deltaStartWatchdog(){
|
|
@@ -22047,7 +23105,7 @@ function _deltaStartWatchdog(){
|
|
|
22047
23105
|
};
|
|
22048
23106
|
S.deltaWatchdogTimer=setTimeout(tick,DELTA_WATCHDOG_INTERVAL_MS);
|
|
22049
23107
|
}
|
|
22050
|
-
function renderSkillsEntryLink(){const link=E('downloadBtn');if(!link)return;const host=location.hostname||'127.0.0.1';const enabled=Boolean(S.config?.skills_ui_enabled);const fromConfig=String(S.config?.skills_ui_url||'').trim();const skillsPort=Number(S.config?.skills_port||0);let href='#';if(enabled){if(fromConfig){href=fromConfig}else if(Number.isFinite(skillsPort)&&skillsPort>0){const currentPort=Number(location.port||0);if(!(currentPort&&skillsPort===currentPort)){href=`${location.protocol}//${host}:${skillsPort}`}}}const offline=(href==='#');link.href=href;link.classList.toggle('disabled',offline);link.textContent=offline?t('skills_offline'):t('open_skills')
|
|
23108
|
+
function renderSkillsEntryLink(){const link=E('downloadBtn');if(!link)return;const host=location.hostname||'127.0.0.1';const enabled=Boolean(S.config?.skills_ui_enabled);const fromConfig=String(S.config?.skills_ui_url||'').trim();const skillsPort=Number(S.config?.skills_port||0);let href='#';if(enabled){if(fromConfig){href=fromConfig}else if(Number.isFinite(skillsPort)&&skillsPort>0){const currentPort=Number(location.port||0);if(!(currentPort&&skillsPort===currentPort)){href=`${location.protocol}//${host}:${skillsPort}`}}}const offline=(href==='#');link.href=href;link.classList.toggle('disabled',offline);link.textContent=offline?t('skills_offline'):t('open_skills')}
|
|
22051
23109
|
function tailSig(rows,count,mapper){const arr=Array.isArray(rows)?rows:[];if(!arr.length)return'';return arr.slice(Math.max(0,arr.length-count)).map(mapper).join('|')}
|
|
22052
23110
|
function feedSignature(snap){const feed=Array.isArray(snap?.conversation_feed)?snap.conversation_feed:(Array.isArray(snap?.messages)?snap.messages:[]);const sig=tailSig(feed,8,row=>`${Number(row?.ts||0)}:${String(row?.role||'')}:${String(row?.agent_role||'')}:${String(row?.type||'')}:${String(row?.text||'').length}:${String(row?.thinking||'').length}:${String(row?.text||'').slice(-12)}:${String(row?.thinking||'').slice(-12)}`);const live=String(snap?.live_thinking||'');const runActive=snap?.live_run_notice_active?1:0;const runLabel=String(snap?.live_run_notice_label||'');const runStart=Number(snap?.live_run_notice_started_at||0);const truncText=String(snap?.live_truncation_text||'');const truncKind=String(snap?.live_truncation_kind||'');const truncTool=String(snap?.live_truncation_tool||'');const truncAttempts=Number(snap?.live_truncation_attempts||0);const truncTokens=Number(snap?.live_truncation_tokens||0);const truncActive=snap?.live_truncation_active?1:0;return `${feed.length}|${sig}|lt=${live.length}:${live.slice(-12)}|rn=${runActive}:${runStart}:${runLabel.slice(-12)}|tr=${truncActive}:${truncAttempts}:${truncTokens}:${truncKind.slice(-12)}:${truncTool.slice(-12)}:${truncText.length}`}
|
|
22053
23111
|
function boardsSignature(snap){return [snap?.running?1:0,snap?.agent_phase||'',Number(snap?.agent_round_index||0),Number(snap?.queued_user_inputs_count||0),Number(snap?.truncation_count||0),Number(snap?.live_truncation_attempts||0),Number(snap?.live_truncation_tokens||0),snap?.live_truncation_active?1:0,Number(snap?.context_tokens_estimate||0),Number(snap?.context_left_tokens||0),Number(snap?.context_left_percent||0),Number(snap?.render_bridge?.seq||0),(snap?.todos||[]).length,(snap?.tasks||[]).length,(snap?.activity||[]).length,(snap?.operations||[]).length,(snap?.uploads||[]).length].join('|')}
|
|
@@ -22066,11 +23124,12 @@ function _scrollContainerToNodeCenter(container,target){
|
|
|
22066
23124
|
}
|
|
22067
23125
|
function _bindNestedScrollGuards(root){
|
|
22068
23126
|
if(!root)return;
|
|
23127
|
+
const SEL='.msg-diff-shell,.msg-code-shell,.preview-code-scroll,.md-code';
|
|
22069
23128
|
const nodes=[];
|
|
22070
|
-
if(root.matches&&root.matches(
|
|
23129
|
+
if(root.matches&&root.matches(SEL)){
|
|
22071
23130
|
nodes.push(root);
|
|
22072
23131
|
}
|
|
22073
|
-
for(const n of root.querySelectorAll(
|
|
23132
|
+
for(const n of root.querySelectorAll(SEL)){
|
|
22074
23133
|
nodes.push(n);
|
|
22075
23134
|
}
|
|
22076
23135
|
const markManualCenterOff=(node)=>{
|
|
@@ -22087,6 +23146,17 @@ function _bindNestedScrollGuards(root){
|
|
|
22087
23146
|
if(key)S.diffCenterDisabled[key]=1;
|
|
22088
23147
|
}
|
|
22089
23148
|
};
|
|
23149
|
+
const markChatUserScrolling=()=>{
|
|
23150
|
+
const chatEl=E('chat');
|
|
23151
|
+
if(!chatEl)return;
|
|
23152
|
+
const now=Date.now();
|
|
23153
|
+
chatEl._virtManualUnlockTs=Math.max(
|
|
23154
|
+
Number(chatEl._virtManualUnlockTs||0),
|
|
23155
|
+
now+CHAT_SCROLL_LOCK_MS
|
|
23156
|
+
);
|
|
23157
|
+
S.follow.chat=false;
|
|
23158
|
+
chatEl._virtAutoFollowPaused=true;
|
|
23159
|
+
};
|
|
22090
23160
|
for(const node of nodes){
|
|
22091
23161
|
if(!node||node._nestedScrollGuardBound)continue;
|
|
22092
23162
|
node._nestedScrollGuardBound=true;
|
|
@@ -22097,16 +23167,38 @@ function _bindNestedScrollGuards(root){
|
|
|
22097
23167
|
const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
|
|
22098
23168
|
const top=Number(node.scrollTop||0);
|
|
22099
23169
|
const left=Number(node.scrollLeft||0);
|
|
22100
|
-
const canY=(dy<0&&top>0)||(dy>0&&top<maxTop);
|
|
22101
|
-
const canX=(dx<0&&left>0)||(dx>0&&left<maxLeft);
|
|
23170
|
+
const canY=(dy<0&&top>0.5)||(dy>0&&top<maxTop-0.5);
|
|
23171
|
+
const canX=(dx<0&&left>0.5)||(dx>0&&left<maxLeft-0.5);
|
|
22102
23172
|
markManualCenterOff(node);
|
|
22103
23173
|
if(canY||canX){
|
|
22104
23174
|
ev.stopPropagation();
|
|
23175
|
+
markChatUserScrolling();
|
|
22105
23176
|
}
|
|
23177
|
+
},{passive:false});
|
|
23178
|
+
node.addEventListener('mousedown',()=>{markManualCenterOff(node);markChatUserScrolling();},{passive:true});
|
|
23179
|
+
node.addEventListener('touchstart',ev=>{
|
|
23180
|
+
markManualCenterOff(node);
|
|
23181
|
+
markChatUserScrolling();
|
|
23182
|
+
node._touchStartY=Number(ev.touches?.[0]?.clientY||0);
|
|
23183
|
+
node._touchStartX=Number(ev.touches?.[0]?.clientX||0);
|
|
22106
23184
|
},{passive:true});
|
|
22107
|
-
node.addEventListener('
|
|
22108
|
-
|
|
22109
|
-
|
|
23185
|
+
node.addEventListener('touchmove',ev=>{
|
|
23186
|
+
markManualCenterOff(node);
|
|
23187
|
+
const curY=Number(ev.touches?.[0]?.clientY||0);
|
|
23188
|
+
const curX=Number(ev.touches?.[0]?.clientX||0);
|
|
23189
|
+
const dy=curY-(node._touchStartY||0);
|
|
23190
|
+
const dx=curX-(node._touchStartX||0);
|
|
23191
|
+
const maxTop=Math.max(0,Number(node.scrollHeight||0)-Number(node.clientHeight||0));
|
|
23192
|
+
const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
|
|
23193
|
+
const top=Number(node.scrollTop||0);
|
|
23194
|
+
const left=Number(node.scrollLeft||0);
|
|
23195
|
+
const canY=(dy>0&&top>0.5)||(dy<0&&top<maxTop-0.5);
|
|
23196
|
+
const canX=(dx>0&&left>0.5)||(dx<0&&left<maxLeft-0.5);
|
|
23197
|
+
if(canY||canX){
|
|
23198
|
+
ev.stopPropagation();
|
|
23199
|
+
}
|
|
23200
|
+
markChatUserScrolling();
|
|
23201
|
+
},{passive:false});
|
|
22110
23202
|
}
|
|
22111
23203
|
}
|
|
22112
23204
|
function _centerDiffShellToHotspot(root){
|
|
@@ -22136,8 +23228,7 @@ function _centerDiffShellToHotspot(root){
|
|
|
22136
23228
|
const target=lines[bestCenter];
|
|
22137
23229
|
if(!target)return;
|
|
22138
23230
|
if(msgKey)shell.setAttribute('data-centered-key',msgKey);
|
|
22139
|
-
|
|
22140
|
-
if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
|
|
23231
|
+
try{_scrollContainerToNodeCenter(shell,target);if(msgKey)S.diffCenteredDone[msgKey]=1;}catch(_){}
|
|
22141
23232
|
}
|
|
22142
23233
|
function splitTableRow(line){const src=String(line||'').trim().replace(/^\\|/,'').replace(/\\|$/,'');if(!src)return[];return src.split('|').map(x=>String(x||'').trim())}
|
|
22143
23234
|
function isTableSeparator(line){const cells=splitTableRow(line);if(!cells.length)return false;return cells.every(cell=>/^:?-{3,}:?$/.test(cell))}
|
|
@@ -22509,7 +23600,7 @@ function _previewRenderStageSelector(tab,stages,selectedReq,payload=null){
|
|
|
22509
23600
|
stat.textContent=`stage ${idx}/${total} · +${add}/-${del}${lineTail}`;
|
|
22510
23601
|
}
|
|
22511
23602
|
function _previewLangFromPath(path){const rel=normalizePreviewPath(path).toLowerCase();const name=rel.split('/').pop()||'';const dot=name.lastIndexOf('.');const ext=dot>=0?name.slice(dot):'';return CODE_LANG_BY_EXT[ext]||CODE_LANG_BY_NAME[name]||'default'}
|
|
22512
|
-
function _codeLangConfig(lang){const v=String(lang||'default');if(v==='python'||v==='shell'||v==='ruby'||v==='yaml'||v==='toml'||v==='ini'||v==='r'||v==='perl'||v==='makefile'||v==='cmake')return{hashComment:true,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='sql')return{hashComment:false,slashComment:false,dashComment:true,blockComment:true,xmlComment:false,backtick:false};if(v==='xml')return{hashComment:false,slashComment:false,dashComment:
|
|
23603
|
+
function _codeLangConfig(lang){const v=String(lang||'default');if(v==='python'||v==='shell'||v==='ruby'||v==='yaml'||v==='toml'||v==='ini'||v==='r'||v==='perl'||v==='makefile'||v==='cmake'||v==='nim'||v==='julia'||v==='elixir'||v==='crystal')return{hashComment:true,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='sql'||v==='haskell')return{hashComment:false,slashComment:false,dashComment:true,blockComment:true,xmlComment:false,backtick:false};if(v==='xml'||v==='vhdl')return{hashComment:false,slashComment:false,dashComment:true,blockComment:false,xmlComment:true,backtick:false};if(v==='json'||v==='fortran'||v==='asm'||v==='protobuf'||v==='prisma'||v==='graphql'||v==='wgsl')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='lisp'||v==='clojure'||v==='racket')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};return{hashComment:false,slashComment:true,dashComment:false,blockComment:true,xmlComment:false,backtick:true}}
|
|
22513
23604
|
function _codeWordSet(lang){return CODE_KEYWORDS[String(lang||'default')]||CODE_KEYWORDS.default}
|
|
22514
23605
|
function _isWordStart(ch){return /[A-Za-z_$]/.test(ch)}
|
|
22515
23606
|
function _isWordChar(ch){return /[A-Za-z0-9_$]/.test(ch)}
|
|
@@ -22913,8 +24004,7 @@ function _scrollCodePreviewToAnchor(body,anchorLine){
|
|
|
22913
24004
|
target=body.querySelector('.code-row.code-add,.code-row.code-delete')||rows[0];
|
|
22914
24005
|
}
|
|
22915
24006
|
if(!target)return;
|
|
22916
|
-
|
|
22917
|
-
if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
|
|
24007
|
+
try{_scrollContainerToNodeCenter(scrollWrap,target);if(previewKey)S.previewCenteredDone[previewKey]=1;}catch(_){}
|
|
22918
24008
|
}
|
|
22919
24009
|
async function _renderCodePreviewTab(tab,body,forceReload=false){
|
|
22920
24010
|
const ticket=String(++S.previewNonce);
|
|
@@ -23032,7 +24122,7 @@ function _chatVirtRowKey(row,idx){const r=row||{};const txt=String(r.text||'');c
|
|
|
23032
24122
|
function _chatVirtFormatElapsed(seconds){const sec=Math.max(0,Math.floor(Number(seconds)||0));const h=Math.floor(sec/3600);const m=Math.floor((sec%3600)/60);const s=sec%60;if(h>0)return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;return `${m}:${String(s).padStart(2,'0')}`}
|
|
23033
24123
|
function _chatVirtLiveRunText(label,elapsed){return `${t('running')} · ${_chatVirtFormatElapsed(elapsed)}`}
|
|
23034
24124
|
function _chatVirtStopRunTicker(chatEl){if(!chatEl)return;const timer=Number(chatEl._virtRunTicker||0);if(timer){clearInterval(timer);chatEl._virtRunTicker=0}}
|
|
23035
|
-
function _chatVirtTickRunNotice(chatEl){if(!chatEl)return;const runActive=!!(S.snap?.running&&S.snap?.live_run_notice_active);if(!runActive){_chatVirtStopRunTicker(chatEl);return}
|
|
24125
|
+
function _chatVirtTickRunNotice(chatEl){if(!chatEl)return;const runActive=!!(S.snap?.running&&S.snap?.live_run_notice_active);if(!runActive){_chatVirtStopRunTicker(chatEl);return}if(!chatEl._virtRunNodes||!chatEl._virtRunNodes.length){chatEl._virtRunNodes=Array.from(chatEl.querySelectorAll('.msg[data-run-live="1"]'))}const nodes=chatEl._virtRunNodes;if(!nodes.length){_chatVirtStopRunTicker(chatEl);return}const now=(Date.now()/1000);for(const node of nodes){const pre=node.querySelector('pre');if(!pre)continue;const label=String(node.getAttribute('data-run-label')||'model call');const startedAt=Number(node.getAttribute('data-run-start')||0);const baseElapsed=Math.max(0,Number(node.getAttribute('data-run-elapsed')||0));const anchorTs=Number(node.getAttribute('data-run-anchor')||0);let elapsed=baseElapsed;if(startedAt>0){elapsed=Math.max(baseElapsed,now-startedAt)}else if(anchorTs>0){elapsed=Math.max(0,baseElapsed+(now-anchorTs))}const whole=Math.floor(elapsed);if(String(node.getAttribute('data-run-last-sec')||'')===String(whole))continue;node.setAttribute('data-run-last-sec',String(whole));pre.textContent=_chatVirtLiveRunText(label,elapsed)}}
|
|
23036
24126
|
function _chatVirtSyncRunTicker(chatEl){if(!chatEl)return;const hasRun=!!chatEl.querySelector('.msg[data-run-live=\"1\"]');if(!hasRun){_chatVirtStopRunTicker(chatEl);return}_chatVirtTickRunNotice(chatEl);if(!chatEl._virtRunTicker){chatEl._virtRunTicker=setInterval(()=>_chatVirtTickRunNotice(chatEl),1000)}}
|
|
23037
24127
|
function _chatVirtCollectRows(){
|
|
23038
24128
|
const feed=Array.isArray(S.snap?.conversation_feed)?S.snap.conversation_feed:(Array.isArray(S.snap?.messages)?S.snap.messages:[]);
|
|
@@ -23101,7 +24191,7 @@ function _chatVirtAcquireNode(kind){const key=String(kind||'text');const pool=_c
|
|
|
23101
24191
|
function _chatVirtReleaseNode(node){
|
|
23102
24192
|
if(!node)return;
|
|
23103
24193
|
const key=String(node.getAttribute('data-pool-kind')||'text');
|
|
23104
|
-
if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||
|
|
24194
|
+
if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||180))return;
|
|
23105
24195
|
if(S.mathObserver){
|
|
23106
24196
|
try{S.mathObserver.unobserve(node)}catch(_){}
|
|
23107
24197
|
}
|
|
@@ -23316,6 +24406,70 @@ function _chatVirtBuildMessageNode(m){
|
|
|
23316
24406
|
return d;
|
|
23317
24407
|
}
|
|
23318
24408
|
function _chatVirtFindWindow(rows,top,bottom){const startTarget=Math.max(0,top-CHAT_VIRT.overscanPx);const endTarget=Math.max(0,bottom+CHAT_VIRT.overscanPx);let start=0;let acc=0;while(start<rows.length){const h=_chatVirtEstimatedHeight(rows[start]);if((acc+h)>=startTarget)break;acc+=h;start+=1}let end=start;let accEnd=acc;while(end<rows.length&&accEnd<=endTarget){accEnd+=_chatVirtEstimatedHeight(rows[end]);end+=1}end=Math.min(rows.length,end+2);return {start,end,topOffset:acc,endOffset:accEnd}}
|
|
24409
|
+
function _chatVirtReuseWindow(chatEl,rows,top,bottom){
|
|
24410
|
+
if(!chatEl||!Array.isArray(rows)||!rows.length)return null;
|
|
24411
|
+
const prevRows=Array.isArray(chatEl._virtLastRows)?chatEl._virtLastRows:[];
|
|
24412
|
+
const prevStart=Number(chatEl._virtLastWinStart||-1);
|
|
24413
|
+
const prevEnd=Number(chatEl._virtLastWinEnd||-1);
|
|
24414
|
+
const prevTopOffset=Number(chatEl._virtLastTopOffset);
|
|
24415
|
+
const prevEndOffset=Number(chatEl._virtLastEndOffset);
|
|
24416
|
+
if(prevStart<0||prevEnd<=prevStart)return null;
|
|
24417
|
+
if(!Number.isFinite(prevTopOffset)||!Number.isFinite(prevEndOffset)||prevEndOffset<=prevTopOffset)return null;
|
|
24418
|
+
if(prevRows.length!==rows.length)return null;
|
|
24419
|
+
const prevFirstKey=String(prevRows[prevStart]?._vk||'');
|
|
24420
|
+
const nextFirstKey=String(rows[prevStart]?._vk||'');
|
|
24421
|
+
const prevLastKey=String(prevRows[Math.max(0,prevEnd-1)]?._vk||'');
|
|
24422
|
+
const nextLastKey=String(rows[Math.max(0,prevEnd-1)]?._vk||'');
|
|
24423
|
+
if(prevFirstKey!==nextFirstKey||prevLastKey!==nextLastKey)return null;
|
|
24424
|
+
const viewport=Math.max(0,bottom-top);
|
|
24425
|
+
const innerPad=Math.max(120,Math.min(Math.round(CHAT_VIRT.overscanPx*0.45),Math.round(viewport*0.35)));
|
|
24426
|
+
if((prevEndOffset-prevTopOffset)<(viewport+(innerPad*2)))return null;
|
|
24427
|
+
const safeTop=prevTopOffset+innerPad;
|
|
24428
|
+
const safeBottom=prevEndOffset-innerPad;
|
|
24429
|
+
if(top>=safeTop&&bottom<=safeBottom){
|
|
24430
|
+
return {start:prevStart,end:prevEnd,topOffset:prevTopOffset,endOffset:prevEndOffset};
|
|
24431
|
+
}
|
|
24432
|
+
return null;
|
|
24433
|
+
}
|
|
24434
|
+
function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
|
|
24435
|
+
function _chatVirtFindRenderedNode(chatEl,key){
|
|
24436
|
+
if(!chatEl||!key)return null;
|
|
24437
|
+
for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
|
|
24438
|
+
if(String(node.getAttribute('data-vk')||'')===String(key||''))return node;
|
|
24439
|
+
}
|
|
24440
|
+
return null;
|
|
24441
|
+
}
|
|
24442
|
+
function _chatVirtCaptureAnchor(chatEl){
|
|
24443
|
+
if(!chatEl)return null;
|
|
24444
|
+
const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
|
|
24445
|
+
let fallback=null;
|
|
24446
|
+
for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
|
|
24447
|
+
const key=String(node.getAttribute('data-vk')||'').trim();
|
|
24448
|
+
if(!key)continue;
|
|
24449
|
+
const rect=node.getBoundingClientRect();
|
|
24450
|
+
const top=Number(rect.top||0)-viewportTop;
|
|
24451
|
+
const bottom=Number(rect.bottom||0)-viewportTop;
|
|
24452
|
+
const anchor={key:key,offset:top};
|
|
24453
|
+
if(!fallback)fallback=anchor;
|
|
24454
|
+
if(bottom>1)return anchor;
|
|
24455
|
+
}
|
|
24456
|
+
return fallback;
|
|
24457
|
+
}
|
|
24458
|
+
function _chatVirtRestoreAnchor(chatEl,anchor){
|
|
24459
|
+
if(!chatEl||!anchor||!anchor.key)return false;
|
|
24460
|
+
const node=_chatVirtFindRenderedNode(chatEl,anchor.key);
|
|
24461
|
+
if(!node)return false;
|
|
24462
|
+
const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
|
|
24463
|
+
const rect=node.getBoundingClientRect();
|
|
24464
|
+
const currentOffset=Number(rect.top||0)-viewportTop;
|
|
24465
|
+
const delta=currentOffset-Number(anchor.offset||0);
|
|
24466
|
+
if(Math.abs(delta)<0.75)return true;
|
|
24467
|
+
const maxTop=Math.max(0,Number(chatEl.scrollHeight||0)-Number(chatEl.clientHeight||0));
|
|
24468
|
+
const target=Math.max(0,Math.min(Number(chatEl.scrollTop||0)+delta,maxTop));
|
|
24469
|
+
if(Math.abs(target-Number(chatEl.scrollTop||0))<0.75)return true;
|
|
24470
|
+
chatEl.scrollTop=target;
|
|
24471
|
+
return true;
|
|
24472
|
+
}
|
|
23319
24473
|
function _chatVirtBindScroll(chatEl){
|
|
23320
24474
|
if(chatEl._virtBound)return;
|
|
23321
24475
|
chatEl._virtBound=true;
|
|
@@ -23385,28 +24539,11 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23385
24539
|
const now=Date.now();
|
|
23386
24540
|
chatEl._virtLastWheelTs=now;
|
|
23387
24541
|
chatEl._virtLastWheelDy=dy;
|
|
23388
|
-
chatEl._virtInputUnlockTs=Math.max(
|
|
23389
|
-
Number(chatEl._virtInputUnlockTs||0),
|
|
23390
|
-
now+CHAT_SCROLL_INPUT_LOCK_MS
|
|
23391
|
-
);
|
|
23392
|
-
const atBottomBefore=nearBottom(chatEl,6);
|
|
23393
24542
|
if(dy<0){
|
|
23394
|
-
markManual(CHAT_SCROLL_LOCK_MS);
|
|
23395
24543
|
S.follow.chat=false;
|
|
23396
24544
|
return;
|
|
23397
24545
|
}
|
|
23398
|
-
if(
|
|
23399
|
-
markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.45));
|
|
23400
|
-
S.follow.chat=false;
|
|
23401
|
-
return;
|
|
23402
|
-
}
|
|
23403
|
-
S.follow.chat=true;
|
|
23404
|
-
chatEl._virtManualUnlockTs=0;
|
|
23405
|
-
chatEl._virtInputUnlockTs=0;
|
|
23406
|
-
chatEl._virtTouchUnlockTs=0;
|
|
23407
|
-
if(atBottomBefore){
|
|
23408
|
-
chatEl._virtAutoFollowPaused=false;
|
|
23409
|
-
}
|
|
24546
|
+
if(nearBottom(chatEl,6))S.follow.chat=true;
|
|
23410
24547
|
},{passive:true});
|
|
23411
24548
|
chatEl.addEventListener('mousedown',()=>{markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.9))},{passive:true});
|
|
23412
24549
|
chatEl.addEventListener('touchstart',()=>{markTouchStart(CHAT_TOUCH_SCROLL_LOCK_MS)},{passive:true});
|
|
@@ -23420,9 +24557,7 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23420
24557
|
chatEl._virtScrollDirection=(curTop>prevTop)?1:((curTop<prevTop)?-1:0);
|
|
23421
24558
|
chatEl._virtLastScrollTop=curTop;
|
|
23422
24559
|
const atBottom=nearBottom(chatEl,6);
|
|
23423
|
-
|
|
23424
|
-
const recentUpIntent=(now-Number(chatEl._virtLastWheelTs||0))<220&&Number(chatEl._virtLastWheelDy||0)<0;
|
|
23425
|
-
if(atBottom&&!manualLock&&!recentUpIntent){
|
|
24560
|
+
if(atBottom){
|
|
23426
24561
|
S.follow.chat=true;
|
|
23427
24562
|
chatEl._virtManualUnlockTs=0;
|
|
23428
24563
|
chatEl._virtInputUnlockTs=0;
|
|
@@ -23433,9 +24568,6 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23433
24568
|
if(S.snap?.running){
|
|
23434
24569
|
chatEl._virtAutoFollowPaused=true;
|
|
23435
24570
|
}
|
|
23436
|
-
if(!atBottom||recentUpIntent){
|
|
23437
|
-
chatEl._virtManualUnlockTs=Math.max(Number(chatEl._virtManualUnlockTs||0),now+CHAT_SCROLL_LOCK_MS);
|
|
23438
|
-
}
|
|
23439
24571
|
}
|
|
23440
24572
|
scheduleScrollRender();
|
|
23441
24573
|
});
|
|
@@ -23453,12 +24585,9 @@ function renderChat(reason='snapshot'){
|
|
|
23453
24585
|
if((!S.snap?.running)&&atBottomNow){
|
|
23454
24586
|
c._virtAutoFollowPaused=false;
|
|
23455
24587
|
}
|
|
23456
|
-
const
|
|
23457
|
-
const manualLock=Number(c._virtManualUnlockTs||0)>now;
|
|
23458
|
-
const autoPaused=Boolean(c._virtAutoFollowPaused);
|
|
23459
|
-
const scrolling=_chatVirtIsUserScrolling(c);
|
|
23460
|
-
const keep=first||(!manualLock&&!autoPaused&&!scrolling&&(atBottomNow||Boolean(S.follow.chat)));
|
|
24588
|
+
const keep=first||Boolean(S.follow.chat)||atBottomNow;
|
|
23461
24589
|
const oldScrollTop=Number(c.scrollTop||0);
|
|
24590
|
+
const anchor=(!keep&&!first)?_chatVirtCaptureAnchor(c):null;
|
|
23462
24591
|
const feedSig=String(S.lastFeedSig||feedSignature(S.snap||{}));
|
|
23463
24592
|
let rows=[];
|
|
23464
24593
|
if(reason==='scroll'&&Array.isArray(c._virtRowsCacheRows)&&String(c._virtRowsCacheSig||'')===feedSig){
|
|
@@ -23474,7 +24603,7 @@ function renderChat(reason='snapshot'){
|
|
|
23474
24603
|
const prevWinEnd=Number(c._virtLastWinEnd||-1);
|
|
23475
24604
|
const top=Math.max(0,c.scrollTop);
|
|
23476
24605
|
const bottom=top+Math.max(0,c.clientHeight||0);
|
|
23477
|
-
const win=_chatVirtFindWindow(rows,top,bottom);
|
|
24606
|
+
const win=((reason==='scroll')?_chatVirtReuseWindow(c,rows,top,bottom):null)||_chatVirtFindWindow(rows,top,bottom);
|
|
23478
24607
|
const totalKey=`${feedSig}|hv=${Number(CHAT_VIRT.heightVersion||0)}|rows=${rows.length}`;
|
|
23479
24608
|
let totalEstimated=0;
|
|
23480
24609
|
if(reason==='scroll'&&String(c._virtTotalKey||'')===totalKey){
|
|
@@ -23592,19 +24721,19 @@ function renderChat(reason='snapshot'){
|
|
|
23592
24721
|
CHAT_VIRT.heightVersion=Number(CHAT_VIRT.heightVersion||0)+1;
|
|
23593
24722
|
}
|
|
23594
24723
|
}
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23600
|
-
c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
|
|
23601
|
-
}
|
|
24724
|
+
const maxTop=Math.max(0,c.scrollHeight-c.clientHeight);
|
|
24725
|
+
if(keep){
|
|
24726
|
+
c.scrollTop=maxTop;
|
|
24727
|
+
}else if(!(anchor&&_chatVirtRestoreAnchor(c,anchor))){
|
|
24728
|
+
c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
|
|
23602
24729
|
}
|
|
23603
24730
|
c._chatHasRendered=true;
|
|
23604
24731
|
c._virtRendering=false;
|
|
23605
24732
|
c._virtLastRows=rows;
|
|
23606
24733
|
c._virtLastWinStart=win.start;
|
|
23607
24734
|
c._virtLastWinEnd=win.end;
|
|
24735
|
+
c._virtLastTopOffset=Number(win.topOffset||0);
|
|
24736
|
+
c._virtLastEndOffset=Number(win.endOffset||0);
|
|
23608
24737
|
if(hasHeightChange&&reason!=='scroll'){
|
|
23609
24738
|
if(c._virtMeasureRaf)cancelAnimationFrame(c._virtMeasureRaf);
|
|
23610
24739
|
c._virtMeasureRaf=requestAnimationFrame(()=>{c._virtMeasureRaf=0;renderChat('measure')});
|
|
@@ -23654,8 +24783,14 @@ refreshFileExplorer(false).catch(()=>{});
|
|
|
23654
24783
|
const uploads=(S.snap?.uploads||[]).slice(-8).reverse();
|
|
23655
24784
|
E('uploadList').innerHTML=uploads.map(u=>`<div>${esc(u.filename)} → ${esc(u.workspace_path||'')} (${esc(u.kind||'')}, ${esc(u.size||0)}B)</div>`).join('')||`<div>${esc(t('no_uploads'))}</div>`;
|
|
23656
24785
|
const sessionZip=S.activeId?('/api/sessions/'+S.activeId+'/export.zip'):'#';
|
|
24786
|
+
const sessionMd=S.activeId?('/api/sessions/'+S.activeId+'/export.md'):'#';
|
|
24787
|
+
const sessionPdf=S.activeId?('/api/sessions/'+S.activeId+'/export.pdf'):'#';
|
|
24788
|
+
const sessionPng=S.activeId?('/api/sessions/'+S.activeId+'/export.png'):'#';
|
|
23657
24789
|
const dl1=E('downloadSessionBtn');
|
|
23658
|
-
|
|
24790
|
+
const dlMd=E('exportMdBtn');
|
|
24791
|
+
const dlPdf=E('exportPdfBtn');
|
|
24792
|
+
const dlPng=E('exportPngBtn');
|
|
24793
|
+
if(S.activeId){dl1.href=sessionZip;if(dlMd)dlMd.href=sessionMd;if(dlPdf)dlPdf.href=sessionPdf;if(dlPng)dlPng.href=sessionPng}else{dl1.href='#';if(dlMd)dlMd.href='#';if(dlPdf)dlPdf.href='#';if(dlPng)dlPng.href='#'}
|
|
23659
24794
|
renderSkillsEntryLink()}
|
|
23660
24795
|
function _normalizeModelCatalog(cat){const src=(cat&&typeof cat==='object')?cat:{};const options=Array.isArray(src.options)?src.options.map(it=>{const row=(it&&typeof it==='object')?it:{};const sel=String(row.selection||'').trim();const mdl=String(row.model||'').trim();if(!sel&&!mdl)return null;const profileId=String(row.profile_id||'').trim()||'profile';const selection=sel||`${profileId}::${mdl}`;return{...row,selection,label:String(row.label||selection)}}).filter(Boolean):[];const models=Array.isArray(src.models)?src.models.map(x=>String(x||'').trim()).filter(Boolean):[];const selected=String(src.selected||'').trim();const thinking=('thinking'in src)?!!src.thinking:null;return{options,models,selected,thinking}}
|
|
23661
24796
|
function _modelNameFromSelection(selection){const raw=String(selection||'').trim();if(!raw)return'';if(raw.includes('::')){const parts=raw.split('::',2);return String(parts[1]||parts[0]||'').trim()}return raw}
|
|
@@ -23753,7 +24888,7 @@ function _chatVirtDebounceWhileScrolling(chatEl,timerField,fn,delayMs=CHAT_SCROL
|
|
|
23753
24888
|
if(!_chatVirtIsUserScrolling(chatEl))done();
|
|
23754
24889
|
};
|
|
23755
24890
|
chatEl[scrollEndField]=onScrollEnd;
|
|
23756
|
-
|
|
24891
|
+
chatEl.addEventListener('scrollend',onScrollEnd,{once:true,passive:true});
|
|
23757
24892
|
}
|
|
23758
24893
|
}
|
|
23759
24894
|
async function refreshSnapshot(opt={}){
|
|
@@ -23801,23 +24936,21 @@ async function refreshSnapshot(opt={}){
|
|
|
23801
24936
|
const chatEl=E('chat');
|
|
23802
24937
|
const scrolling=_chatVirtIsUserScrolling(chatEl);
|
|
23803
24938
|
const feedSig=feedSignature(S.snap);
|
|
23804
|
-
if(forceFull||feedSig!==S.lastFeedSig){
|
|
23805
|
-
S.lastFeedSig=feedSig;
|
|
23806
|
-
if(scrolling&&chatEl){
|
|
23807
|
-
_chatVirtDebounceWhileScrolling(chatEl,'_virtScrollSyncTimer',()=>renderChat('snapshot'));
|
|
23808
|
-
}else{
|
|
23809
|
-
if(chatEl)_chatVirtCancelDebounce(chatEl,'_virtScrollSyncTimer');
|
|
23810
|
-
renderChat();
|
|
23811
|
-
}
|
|
23812
|
-
}
|
|
23813
24939
|
const boardSig=boardsSignature(S.snap);
|
|
23814
|
-
|
|
23815
|
-
|
|
24940
|
+
const needChat=forceFull||feedSig!==S.lastFeedSig;
|
|
24941
|
+
const needBoards=forceFull||boardSig!==S.lastBoardsSig;
|
|
24942
|
+
if(needChat)S.lastFeedSig=feedSig;
|
|
24943
|
+
if(needBoards)S.lastBoardsSig=boardSig;
|
|
24944
|
+
if(needChat||needBoards){
|
|
24945
|
+
const doRender=()=>{
|
|
24946
|
+
if(needChat)renderChat('snapshot');
|
|
24947
|
+
if(needBoards)renderBoards();
|
|
24948
|
+
};
|
|
23816
24949
|
if(scrolling&&chatEl){
|
|
23817
|
-
_chatVirtDebounceWhileScrolling(chatEl,'
|
|
24950
|
+
_chatVirtDebounceWhileScrolling(chatEl,'_virtScrollSyncTimer',doRender);
|
|
23818
24951
|
}else{
|
|
23819
|
-
if(chatEl)_chatVirtCancelDebounce(chatEl,'_virtBoardsSyncTimer');
|
|
23820
|
-
|
|
24952
|
+
if(chatEl){_chatVirtCancelDebounce(chatEl,'_virtScrollSyncTimer');_chatVirtCancelDebounce(chatEl,'_virtBoardsSyncTimer');}
|
|
24953
|
+
doRender();
|
|
23821
24954
|
}
|
|
23822
24955
|
}
|
|
23823
24956
|
renderActivePreview(false);
|
|
@@ -23903,7 +25036,7 @@ async function compactNow(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resu
|
|
|
23903
25036
|
async function clearStaleTodos(){if(!S.activeId){showError(t('select_session_first'));return}if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/todos/clear-stale',{method:'POST'});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:160,allowWhenFrozen:true})}}
|
|
23904
25037
|
async function refreshAll(forceProbe=false){if(S.staticMode&&S.frozen){S.frozen=false;applyStaticUiClass()}S.config=await api('/api/config');renderLanguageControls();applyMainI18n();S.skills=await api('/api/skills');S.tools=await api('/api/tools');S.providers=await api('/api/skills/providers');S.protocols=await api('/api/skills/protocols');renderSkillsEntryLink();await refreshSessions();const mc=await loadModelCatalog(forceProbe);if(!applyModelCatalog(mc)){renderModelControls()}if(S.activeId)await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}
|
|
23905
25038
|
function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
|
|
23906
|
-
window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){const el=E(id);if(el){if(id==='chat'){continue}if(id==='sessionList'||id==='todos'||id==='tasks'){S.follow[id]=false;const mark=(lockMs=PANEL_SCROLL_ACTIVE_MS)=>{const now=Date.now();el._panelUserScrollTs=now;el._panelUserScrollLockTs=Math.max(Number(el._panelUserScrollLockTs||0),now+Math.max(PANEL_SCROLL_ACTIVE_MS,Number(lockMs)||PANEL_SCROLL_ACTIVE_MS))};el.addEventListener('wheel',()=>mark(PANEL_SCROLL_ACTIVE_MS+260),{passive:true});el.addEventListener('touchstart',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('touchmove',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('mousedown',()=>mark(PANEL_SCROLL_ACTIVE_MS+180),{passive:true});el.addEventListener('scroll',()=>mark(PANEL_SCROLL_ACTIVE_MS),{passive:true});continue}el.addEventListener('scroll',()=>{S.follow[id]=nearBottom(el)})}}const drop=E('uploadDrop');const fileInput=E('uploadInput');if(drop&&fileInput){drop.onclick=()=>fileInput.click();fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');uploadFiles(e.dataTransfer?.files||[]).catch(err=>showError(err.message))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('importConfigBtn',importDefaultConfig);bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('compactBtn',compactNow);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('refreshBtn',()=>refreshAll(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}const promptEl=E('prompt');if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length)await createSession()}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
|
|
25039
|
+
window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){const el=E(id);if(el){if(id==='chat'){continue}if(id==='sessionList'||id==='todos'||id==='tasks'){S.follow[id]=false;const mark=(lockMs=PANEL_SCROLL_ACTIVE_MS)=>{const now=Date.now();el._panelUserScrollTs=now;el._panelUserScrollLockTs=Math.max(Number(el._panelUserScrollLockTs||0),now+Math.max(PANEL_SCROLL_ACTIVE_MS,Number(lockMs)||PANEL_SCROLL_ACTIVE_MS))};el.addEventListener('wheel',()=>mark(PANEL_SCROLL_ACTIVE_MS+260),{passive:true});el.addEventListener('touchstart',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('touchmove',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('mousedown',()=>mark(PANEL_SCROLL_ACTIVE_MS+180),{passive:true});el.addEventListener('scroll',()=>mark(PANEL_SCROLL_ACTIVE_MS),{passive:true});continue}el.addEventListener('scroll',()=>{S.follow[id]=nearBottom(el)})}}const drop=E('uploadDrop');const fileInput=E('uploadInput');if(drop&&fileInput){drop.onclick=()=>fileInput.click();fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');uploadFiles(e.dataTransfer?.files||[]).catch(err=>showError(err.message))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('importConfigBtn',importDefaultConfig);bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('compactBtn',compactNow);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('refreshBtn',()=>refreshAll(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const exportMenuBtn=E('exportMenuBtn');const exportMenu=E('exportMenu');if(exportMenuBtn&&exportMenu){exportMenuBtn.addEventListener('click',e=>{e.stopPropagation();exportMenu.style.display=exportMenu.style.display==='none'?'block':'none'});document.addEventListener('click',()=>{exportMenu.style.display='none'});exportMenu.addEventListener('click',e=>{e.stopPropagation()});for(const a of exportMenu.querySelectorAll('.export-item')){a.addEventListener('click',()=>{exportMenu.style.display='none'})}}const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}const promptEl=E('prompt');if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length)await createSession()}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.deltaWatchdogTimer){clearTimeout(S.deltaWatchdogTimer);S.deltaWatchdogTimer=null}if(S.sessionPollTimer){clearTimeout(S.sessionPollTimer);S.sessionPollTimer=null}if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();_deltaStartWatchdog();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
|
|
23907
25040
|
"""
|
|
23908
25041
|
|
|
23909
25042
|
APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
|
|
@@ -23957,7 +25090,7 @@ SKILLS_INDEX_HTML = """<!doctype html>
|
|
|
23957
25090
|
<select id="modelSelect"></select>
|
|
23958
25091
|
<button id="applyModelBtn" class="subtle">Apply Model</button>
|
|
23959
25092
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
23960
|
-
<a id="agentLink" href="#"
|
|
25093
|
+
<a id="agentLink" href="#">Open Agent UI</a>
|
|
23961
25094
|
</div>
|
|
23962
25095
|
</header>
|
|
23963
25096
|
<div class="status-cards" id="topStats"></div>
|
|
@@ -24251,9 +25384,9 @@ function pointToSegmentDistance(px,py,x1,y1,x2,y2){const dx=x2-x1,dy=y2-y1;const
|
|
|
24251
25384
|
function findNearestEdgeIndexAt(px,py,threshold=14){const byId={};for(const n of (S.flow.nodes||[])){byId[n.id]=n}const z=getFlowZoom();let bestIdx=-1;let best=Number.POSITIVE_INFINITY;for(let i=0;i<(S.flow.edges||[]).length;i++){const e=S.flow.edges[i];const rp=resolveEdgeSidesAndPoints(e,byId);if(!rp)continue;const x1=(Number(rp.p1.x)||0)*z,y1=(Number(rp.p1.y)||0)*z,x2=(Number(rp.p2.x)||0)*z,y2=(Number(rp.p2.y)||0)*z;const dLine=pointToSegmentDistance(px,py,x1,y1,x2,y2);const dArrow=Math.hypot(px-x2,py-y2);const d=Math.min(dLine,dArrow);if(d<best){best=d;bestIdx=i}}if(best<=Math.max(6,Number(threshold)||14))return bestIdx;return -1}
|
|
24252
25385
|
function beginLinkDrag(nodeId,side,ev){if(ev){ev.preventDefault();ev.stopPropagation()}const canvas=E('flowCanvas');if(!canvas)return;const rect=canvas.getBoundingClientRect();S.drag=null;S.pan=null;S.selectedNodeId=String(nodeId||'');S.linkDrag={fromId:String(nodeId||''),fromSide:normalizeSide(side)||'right',toX:(ev?ev.clientX:rect.left)-rect.left,toY:(ev?ev.clientY:rect.top)-rect.top};renderNodeEditor();renderFlow()}
|
|
24253
25386
|
function renderFlow(){const canvas=E('flowCanvas');const svg=E('flowSvg');if(!canvas||!svg)return;const z=getFlowZoom();canvas.innerHTML='';svg.innerHTML='';const defs=document.createElementNS('http://www.w3.org/2000/svg','defs');const marker=document.createElementNS('http://www.w3.org/2000/svg','marker');marker.setAttribute('id','arrow');marker.setAttribute('viewBox','0 0 10 10');marker.setAttribute('refX','10');marker.setAttribute('refY','5');marker.setAttribute('markerWidth','7');marker.setAttribute('markerHeight','7');marker.setAttribute('orient','auto-start-reverse');const path=document.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d','M 0 0 L 10 5 L 0 10 z');path.setAttribute('fill','#7f95b8');marker.appendChild(path);defs.appendChild(marker);svg.appendChild(defs);const byId={};let baseW=1300,baseH=900;for(const n of S.flow.nodes){byId[n.id]=n;baseW=Math.max(baseW,(Number(n.x)||0)+230);baseH=Math.max(baseH,(Number(n.y)||0)+190)}const viewW=Math.max(320,Math.floor(baseW*z));const viewH=Math.max(220,Math.floor(baseH*z));canvas.style.minWidth=`${viewW}px`;canvas.style.minHeight=`${viewH}px`;svg.style.minWidth=`${viewW}px`;svg.style.minHeight=`${viewH}px`;svg.setAttribute('width',String(viewW));svg.setAttribute('height',String(viewH));const returnMap={};for(const e of (S.flow.edges||[])){const a=byId[e.from],b=byId[e.to];if(!a||!b)continue;let fs=edgeFromSide(e);let ts=edgeToSide(e);if(!fs||!ts){const autoSides=inferEdgeSides(a,b);if(!fs)fs=autoSides.from;if(!ts)ts=autoSides.to}const pa=nodePortPoint(a,fs);const pb=nodePortPoint(b,ts);const ca={x:pa.x*z,y:pa.y*z};const cb={x:pb.x*z,y:pb.y*z};const bi=edgeBidirectional(e);const line=document.createElementNS('http://www.w3.org/2000/svg','line');line.setAttribute('x1',String(ca.x));line.setAttribute('y1',String(ca.y));line.setAttribute('x2',String(cb.x));line.setAttribute('y2',String(cb.y));line.setAttribute('stroke','#7f95b8');line.setAttribute('stroke-width','1.8');if(bi)line.setAttribute('marker-start','url(#arrow)');line.setAttribute('marker-end','url(#arrow)');svg.appendChild(line);const lbl=document.createElementNS('http://www.w3.org/2000/svg','text');lbl.setAttribute('x',String((ca.x+cb.x)/2));lbl.setAttribute('y',String((ca.y+cb.y)/2-7));lbl.setAttribute('text-anchor','middle');lbl.setAttribute('class','flow-edge-label');lbl.textContent=edgePathLabel(e);svg.appendChild(lbl);if(bi){const n=edgeReturnN(e);const f=String(e.from||'').trim();const t=String(e.to||'').trim();if(f)returnMap[f]=Math.max(Number(returnMap[f]||0),n);if(t)returnMap[t]=Math.max(Number(returnMap[t]||0),n)}}if(S.linkDrag&&byId[S.linkDrag.fromId]){const src=nodePortPoint(byId[S.linkDrag.fromId],S.linkDrag.fromSide);const line=document.createElementNS('http://www.w3.org/2000/svg','line');line.setAttribute('x1',String(src.x*z));line.setAttribute('y1',String(src.y*z));line.setAttribute('x2',String(Number(S.linkDrag.toX)||src.x*z));line.setAttribute('y2',String(Number(S.linkDrag.toY)||src.y*z));line.setAttribute('class','flow-link-preview');svg.appendChild(line)}for(const n of S.flow.nodes){const d=document.createElement('div');d.className='flow-node'+(n.id===S.selectedNodeId?' active':'');d.style.left=((Number(n.x)||0)*z)+'px';d.style.top=((Number(n.y)||0)*z)+'px';d.style.transform=`scale(${z})`;d.style.transformOrigin='top left';d.innerHTML=`<div class=\"k\">${esc(n.type||'node')}</div><div class=\"t\">${esc(n.title||n.id)}</div><div class=\"c\">${esc(n.content||'')}</div>`;d.onmousedown=(ev)=>{ev.preventDefault();S.linkDrag=null;S.selectedNodeId=n.id;const zz=getFlowZoom();S.drag={id:n.id,dx:ev.clientX-((Number(n.x)||0)*zz),dy:ev.clientY-((Number(n.y)||0)*zz)};renderFlow();renderNodeEditor()};d.onclick=()=>{S.selectedNodeId=n.id;renderFlow();renderNodeEditor()};for(const side of FLOW_SIDES){const p=document.createElement('div');p.className='flow-port side-'+side+((S.linkDrag&&S.linkDrag.fromId===n.id&&S.linkDrag.fromSide===side)?' active':'');p.setAttribute('data-node-id',String(n.id));p.setAttribute('data-side',side);p.onmousedown=(ev)=>beginLinkDrag(n.id,side,ev);d.appendChild(p)}const backN=Number(returnMap[String(n.id)]||0);if(backN>0){const badge=document.createElement('div');badge.className='flow-return-badge';badge.textContent='n='+String(backN);d.appendChild(badge)}canvas.appendChild(d)}renderEdgeSelects();scheduleFlowWrapAdjust();updateFlowZoomUI()}
|
|
24254
|
-
let flowWrapRaf=0;
|
|
25387
|
+
let flowWrapRaf=0;let flowWrapDebounce=0;
|
|
24255
25388
|
function adjustFlowWrapHeight(){const wrap=E('flowWrap');const panel=document.querySelector('.skills-panel-center');const stage=wrap&&wrap.closest?wrap.closest('.flow-stage'):null;if(!wrap||!panel||!stage)return;const mobile=(window.matchMedia&&window.matchMedia('(max-width:1180px)').matches);if(mobile){stage.style.height='320px';stage.style.minHeight='320px';wrap.style.height='100%';return}const kids=Array.from(panel.children||[]);let used=0;for(const el of kids){if(el===stage||el===wrap)continue;const st=getComputedStyle(el);used+=el.offsetHeight+(parseFloat(st.marginTop)||0)+(parseFloat(st.marginBottom)||0)}const ps=getComputedStyle(panel);const gap=(parseFloat(ps.rowGap||ps.gap)||0);if(kids.length>1)used+=gap*(kids.length-1);const vh=Math.max(760,window.innerHeight||900);const maxPx=Math.floor(vh*0.58);let available=Math.floor(panel.clientHeight-used);if(!Number.isFinite(available))available=360;available=Math.max(280,Math.min(maxPx,available));stage.style.height=`${available}px`;stage.style.minHeight='260px';wrap.style.height='100%'}
|
|
24256
|
-
function scheduleFlowWrapAdjust(){if(flowWrapRaf)cancelAnimationFrame(flowWrapRaf);flowWrapRaf=requestAnimationFrame(()=>{flowWrapRaf=0;adjustFlowWrapHeight()})}
|
|
25389
|
+
function scheduleFlowWrapAdjust(){if(flowWrapRaf)cancelAnimationFrame(flowWrapRaf);if(flowWrapDebounce)clearTimeout(flowWrapDebounce);flowWrapDebounce=setTimeout(()=>{flowWrapDebounce=0;flowWrapRaf=requestAnimationFrame(()=>{flowWrapRaf=0;adjustFlowWrapHeight()})},60)}
|
|
24257
25390
|
function switchFlowPanel(mode){const m=(String(mode||'').toLowerCase()==='link')?'link':'node';const nodeBtn=E('flowTabNodeBtn');const linkBtn=E('flowTabLinkBtn');const nodePanel=E('flowPanelNode');const linkPanel=E('flowPanelLink');if(nodeBtn)nodeBtn.classList.toggle('active',m==='node');if(linkBtn)linkBtn.classList.toggle('active',m==='link');if(nodePanel)nodePanel.classList.toggle('active',m==='node');if(linkPanel)linkPanel.classList.toggle('active',m==='link');scheduleFlowWrapAdjust()}
|
|
24258
25391
|
function renderEdgeSelects(){const ids=S.flow.nodes.map(n=>n.id);const render=id=>{const el=E(id);if(!el)return;const cur=el.value;el.innerHTML=ids.map(x=>`<option value=\"${esc(x)}\">${esc(x)}</option>`).join('');if(cur&&ids.includes(cur))el.value=cur};render('edgeFrom');render('edgeTo')}
|
|
24259
25392
|
function renderNodeEditor(){const n=S.flow.nodes.find(x=>x.id===S.selectedNodeId)||null;if(!n)return;E('nodeTitle').value=n.title||'';E('nodeType').value=n.type||'process';E('nodeContent').value=n.content||''}
|
|
@@ -24320,6 +25453,7 @@ class AppContext:
|
|
|
24320
25453
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
24321
25454
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
24322
25455
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
25456
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
24323
25457
|
max_user: int = 0,
|
|
24324
25458
|
max_user_sessions: int = 0,
|
|
24325
25459
|
):
|
|
@@ -24364,6 +25498,7 @@ class AppContext:
|
|
|
24364
25498
|
self.arbiter_max_tokens = max(24, min(256, int(arbiter_max_tokens or ARBITER_DEFAULT_MAX_TOKENS)))
|
|
24365
25499
|
self.arbiter_temperature = max(0.0, min(1.0, float(arbiter_temperature if arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)))
|
|
24366
25500
|
self.execution_mode = normalize_execution_mode(execution_mode, default=EXECUTION_MODE_SYNC)
|
|
25501
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
24367
25502
|
self.skills_root = skills_root
|
|
24368
25503
|
ensure_runtime_skills(self.skills_root)
|
|
24369
25504
|
self.skills_store = SkillStore(self.skills_root)
|
|
@@ -24832,6 +25967,7 @@ class AppContext:
|
|
|
24832
25967
|
self.arbiter_max_tokens,
|
|
24833
25968
|
self.arbiter_temperature,
|
|
24834
25969
|
self.execution_mode,
|
|
25970
|
+
self.max_output_tokens,
|
|
24835
25971
|
run_finished_callback=self._on_session_run_finished,
|
|
24836
25972
|
)
|
|
24837
25973
|
self._session_mgrs[user_id] = mgr
|
|
@@ -24913,6 +26049,11 @@ class AppContext:
|
|
|
24913
26049
|
active = dict(self.global_profiles.get(self.global_active_profile_id, {}))
|
|
24914
26050
|
self._sync_global_ollama_defaults(active)
|
|
24915
26051
|
self.thinking = False
|
|
26052
|
+
cfg_max_output_tokens = cfg.get("max_output_tokens")
|
|
26053
|
+
if cfg_max_output_tokens is not None:
|
|
26054
|
+
self.max_output_tokens = max(256, int(cfg_max_output_tokens))
|
|
26055
|
+
if "single_advance_prompt_enhance" in cfg:
|
|
26056
|
+
self.single_advance_prompt_enhance = bool(cfg["single_advance_prompt_enhance"])
|
|
24916
26057
|
|
|
24917
26058
|
def normalized_profiles() -> tuple[dict[str, dict], str]:
|
|
24918
26059
|
rows: dict[str, dict] = {}
|
|
@@ -26007,6 +27148,33 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
26007
27148
|
if not sess:
|
|
26008
27149
|
return self._send_json({"error": "session not found"}, status=404)
|
|
26009
27150
|
return self._send_bytes(sess.export_bundle(), "application/zip", f"{sess.id}_session_export.zip")
|
|
27151
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.md$", path)
|
|
27152
|
+
if m:
|
|
27153
|
+
sess = mgr.get(m.group(1))
|
|
27154
|
+
if not sess:
|
|
27155
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27156
|
+
md = sess.export_conversation_md()
|
|
27157
|
+
return self._send_bytes(md.encode("utf-8"), "text/markdown; charset=utf-8", f"{sess.id}_conversation.md")
|
|
27158
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.pdf$", path)
|
|
27159
|
+
if m:
|
|
27160
|
+
sess = mgr.get(m.group(1))
|
|
27161
|
+
if not sess:
|
|
27162
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27163
|
+
try:
|
|
27164
|
+
pdf = sess.export_conversation_pdf()
|
|
27165
|
+
return self._send_bytes(pdf, "application/pdf", f"{sess.id}_conversation.pdf")
|
|
27166
|
+
except Exception as exc:
|
|
27167
|
+
return self._send_json({"error": str(exc)}, status=500)
|
|
27168
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.png$", path)
|
|
27169
|
+
if m:
|
|
27170
|
+
sess = mgr.get(m.group(1))
|
|
27171
|
+
if not sess:
|
|
27172
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27173
|
+
try:
|
|
27174
|
+
img = sess.export_conversation_image()
|
|
27175
|
+
return self._send_bytes(img, "image/png", f"{sess.id}_conversation.png")
|
|
27176
|
+
except Exception as exc:
|
|
27177
|
+
return self._send_json({"error": str(exc)}, status=500)
|
|
26010
27178
|
return self._send_json({"error": "not found"}, status=404)
|
|
26011
27179
|
|
|
26012
27180
|
def do_POST(self):
|
|
@@ -26640,6 +27808,12 @@ def main():
|
|
|
26640
27808
|
default="",
|
|
26641
27809
|
help="Agent execution mode (single|sequential|sync). Empty means read from startup config, then fallback to sync.",
|
|
26642
27810
|
)
|
|
27811
|
+
parser.add_argument(
|
|
27812
|
+
"--max-output-tokens",
|
|
27813
|
+
default=AGENT_MAX_OUTPUT_TOKENS,
|
|
27814
|
+
type=int,
|
|
27815
|
+
help=f"Max output tokens per agent turn (default: {AGENT_MAX_OUTPUT_TOKENS}). Also configurable via config file key 'max_output_tokens'.",
|
|
27816
|
+
)
|
|
26643
27817
|
parser.add_argument(
|
|
26644
27818
|
"--max_user",
|
|
26645
27819
|
default=None,
|
|
@@ -26858,6 +28032,7 @@ def main():
|
|
|
26858
28032
|
or ""
|
|
26859
28033
|
).strip()
|
|
26860
28034
|
resolved_execution_mode = normalize_execution_mode(raw_execution_mode, default=EXECUTION_MODE_SYNC)
|
|
28035
|
+
resolved_max_output_tokens = max(256, int(getattr(args, "max_output_tokens", AGENT_MAX_OUTPUT_TOKENS) or AGENT_MAX_OUTPUT_TOKENS))
|
|
26861
28036
|
if raw_execution_mode:
|
|
26862
28037
|
normalized_raw = str(raw_execution_mode).strip().lower()
|
|
26863
28038
|
if normalized_raw != resolved_execution_mode:
|
|
@@ -26904,6 +28079,7 @@ def main():
|
|
|
26904
28079
|
resolved_arbiter_max_tokens,
|
|
26905
28080
|
resolved_arbiter_temperature,
|
|
26906
28081
|
resolved_execution_mode,
|
|
28082
|
+
resolved_max_output_tokens,
|
|
26907
28083
|
resolved_max_user,
|
|
26908
28084
|
resolved_max_user_sessions,
|
|
26909
28085
|
)
|