clouds-coder 2026.3.8__tar.gz → 2026.3.16__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.16}/Clouds_Coder.py +1449 -300
- {clouds_coder-2026.3.8/clouds_coder.egg-info → clouds_coder-2026.3.16}/PKG-INFO +4 -4
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/README.md +3 -3
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16/clouds_coder.egg-info}/PKG-INFO +4 -4
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/pyproject.toml +1 -1
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/LICENSE +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/setup.cfg +0 -0
- {clouds_coder-2026.3.8 → clouds_coder-2026.3.16}/tests/test_smoke.py +0 -0
|
@@ -71,7 +71,8 @@ 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
|
|
77
78
|
WATCHDOG_STATE_STALL_THRESHOLD = 6
|
|
@@ -202,7 +203,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
202
203
|
"execution_mode": EXECUTION_MODE_SINGLE,
|
|
203
204
|
"participants": ["developer"],
|
|
204
205
|
"assigned_expert": "developer",
|
|
205
|
-
"round_budget":
|
|
206
|
+
"round_budget": 2,
|
|
206
207
|
"requires_user_confirmation": False,
|
|
207
208
|
"complexity": "simple",
|
|
208
209
|
},
|
|
@@ -211,7 +212,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
211
212
|
"execution_mode": EXECUTION_MODE_SINGLE,
|
|
212
213
|
"participants": ["developer"],
|
|
213
214
|
"assigned_expert": "developer",
|
|
214
|
-
"round_budget":
|
|
215
|
+
"round_budget": 6,
|
|
215
216
|
"requires_user_confirmation": False,
|
|
216
217
|
"complexity": "simple",
|
|
217
218
|
},
|
|
@@ -220,7 +221,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
220
221
|
"execution_mode": EXECUTION_MODE_SYNC,
|
|
221
222
|
"participants": ["explorer", "developer"],
|
|
222
223
|
"assigned_expert": "developer",
|
|
223
|
-
"round_budget":
|
|
224
|
+
"round_budget": 10,
|
|
224
225
|
"requires_user_confirmation": False,
|
|
225
226
|
"complexity": "simple",
|
|
226
227
|
},
|
|
@@ -229,7 +230,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
229
230
|
"execution_mode": EXECUTION_MODE_SYNC,
|
|
230
231
|
"participants": ["explorer", "developer", "reviewer"],
|
|
231
232
|
"assigned_expert": "developer",
|
|
232
|
-
"round_budget":
|
|
233
|
+
"round_budget": 24,
|
|
233
234
|
"requires_user_confirmation": False,
|
|
234
235
|
"complexity": "complex",
|
|
235
236
|
},
|
|
@@ -485,6 +486,30 @@ CODE_PREVIEW_EXTS = {
|
|
|
485
486
|
".svelte",
|
|
486
487
|
".gradle",
|
|
487
488
|
".properties",
|
|
489
|
+
# Fortran
|
|
490
|
+
".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
|
|
491
|
+
# Haskell
|
|
492
|
+
".hs", ".lhs",
|
|
493
|
+
# Erlang / Elixir
|
|
494
|
+
".erl", ".hrl", ".ex", ".exs",
|
|
495
|
+
# OCaml
|
|
496
|
+
".ml", ".mli",
|
|
497
|
+
# HDL
|
|
498
|
+
".vhd", ".vhdl", ".v", ".sv",
|
|
499
|
+
# Assembly
|
|
500
|
+
".asm", ".s",
|
|
501
|
+
# Infra / Schema
|
|
502
|
+
".proto", ".tf", ".tfvars", ".prisma", ".graphql", ".gql",
|
|
503
|
+
# Modern systems
|
|
504
|
+
".zig", ".nim", ".jl", ".cr", ".d",
|
|
505
|
+
# Lisp family
|
|
506
|
+
".clj", ".cljs", ".cljc", ".lisp", ".cl", ".el", ".rkt",
|
|
507
|
+
# Pascal
|
|
508
|
+
".pas", ".pp",
|
|
509
|
+
# Shader
|
|
510
|
+
".wgsl", ".glsl", ".hlsl",
|
|
511
|
+
# Misc
|
|
512
|
+
".groovy", ".cmake", ".dockerfile",
|
|
488
513
|
}
|
|
489
514
|
CODE_PREVIEW_FILENAMES = {
|
|
490
515
|
"dockerfile",
|
|
@@ -1310,6 +1335,117 @@ def trim(text: object, limit: int = MAX_TOOL_OUTPUT) -> str:
|
|
|
1310
1335
|
s = str(text)
|
|
1311
1336
|
return s if len(s) <= limit else s[:limit] + "\n...(truncated)"
|
|
1312
1337
|
|
|
1338
|
+
|
|
1339
|
+
def _fmt_export_ts(ts: float | int) -> str:
|
|
1340
|
+
v = float(ts or 0)
|
|
1341
|
+
if v <= 0:
|
|
1342
|
+
return ""
|
|
1343
|
+
try:
|
|
1344
|
+
from datetime import datetime
|
|
1345
|
+
return datetime.fromtimestamp(v).strftime("%Y-%m-%d %H:%M:%S")
|
|
1346
|
+
except Exception:
|
|
1347
|
+
return ""
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _html_esc(text: str) -> str:
|
|
1351
|
+
return html.escape(str(text or ""))
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _text_to_minimal_pdf(text: str) -> bytes:
|
|
1355
|
+
"""纯 Python 最小 PDF 生成器,无外部依赖。"""
|
|
1356
|
+
raw = str(text or "")
|
|
1357
|
+
lines = raw.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
1358
|
+
font_size = 10
|
|
1359
|
+
leading = font_size * 1.35
|
|
1360
|
+
margin_top, margin_bottom, margin_left, margin_right = 50, 50, 50, 50
|
|
1361
|
+
page_w, page_h = 595, 842 # A4
|
|
1362
|
+
usable_w = page_w - margin_left - margin_right
|
|
1363
|
+
max_chars_per_line = max(20, int(usable_w / (font_size * 0.52)))
|
|
1364
|
+
usable_h = page_h - margin_top - margin_bottom
|
|
1365
|
+
lines_per_page = max(1, int(usable_h / leading))
|
|
1366
|
+
|
|
1367
|
+
# 折行
|
|
1368
|
+
wrapped: list[str] = []
|
|
1369
|
+
for line in lines:
|
|
1370
|
+
if not line:
|
|
1371
|
+
wrapped.append("")
|
|
1372
|
+
continue
|
|
1373
|
+
while len(line) > max_chars_per_line:
|
|
1374
|
+
wrapped.append(line[:max_chars_per_line])
|
|
1375
|
+
line = line[max_chars_per_line:]
|
|
1376
|
+
wrapped.append(line)
|
|
1377
|
+
|
|
1378
|
+
# 分页
|
|
1379
|
+
pages: list[list[str]] = []
|
|
1380
|
+
for i in range(0, len(wrapped), lines_per_page):
|
|
1381
|
+
pages.append(wrapped[i:i + lines_per_page])
|
|
1382
|
+
if not pages:
|
|
1383
|
+
pages = [[""]]
|
|
1384
|
+
|
|
1385
|
+
def _pdf_escape(s: str) -> str:
|
|
1386
|
+
return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
|
1387
|
+
|
|
1388
|
+
def _safe_latin1(s: str) -> str:
|
|
1389
|
+
return s.encode("latin-1", errors="replace").decode("latin-1")
|
|
1390
|
+
|
|
1391
|
+
objects: list[bytes] = []
|
|
1392
|
+
offsets: list[int] = []
|
|
1393
|
+
buf = b"%PDF-1.4\n"
|
|
1394
|
+
|
|
1395
|
+
def add_obj(content: str) -> int:
|
|
1396
|
+
nonlocal buf
|
|
1397
|
+
idx = len(objects) + 1
|
|
1398
|
+
offsets.append(len(buf))
|
|
1399
|
+
obj_bytes = f"{idx} 0 obj\n{content}\nendobj\n".encode("latin-1")
|
|
1400
|
+
buf += obj_bytes
|
|
1401
|
+
objects.append(obj_bytes)
|
|
1402
|
+
return idx
|
|
1403
|
+
|
|
1404
|
+
catalog_id = add_obj("<< /Type /Catalog /Pages 2 0 R >>")
|
|
1405
|
+
add_obj("PAGES_PLACEHOLDER") # obj 2: placeholder, replaced after page generation
|
|
1406
|
+
font_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>")
|
|
1407
|
+
|
|
1408
|
+
page_ids: list[int] = []
|
|
1409
|
+
for page_lines in pages:
|
|
1410
|
+
stream_lines = [f"BT /F1 {font_size} Tf"]
|
|
1411
|
+
y = page_h - margin_top
|
|
1412
|
+
stream_lines.append(f"{margin_left} {y} Td")
|
|
1413
|
+
for pl in page_lines:
|
|
1414
|
+
safe = _safe_latin1(_pdf_escape(pl))
|
|
1415
|
+
stream_lines.append(f"({safe}) Tj")
|
|
1416
|
+
stream_lines.append(f"0 -{leading:.1f} Td")
|
|
1417
|
+
stream_lines.append("ET")
|
|
1418
|
+
stream = "\n".join(stream_lines)
|
|
1419
|
+
stream_bytes = stream.encode("latin-1")
|
|
1420
|
+
content_id = add_obj(f"<< /Length {len(stream_bytes)} >>\nstream\n{stream}\nendstream")
|
|
1421
|
+
page_id = add_obj(
|
|
1422
|
+
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {page_w} {page_h}] "
|
|
1423
|
+
f"/Contents {content_id} 0 R /Resources << /Font << /F1 {font_id} 0 R >> >> >>"
|
|
1424
|
+
)
|
|
1425
|
+
page_ids.append(page_id)
|
|
1426
|
+
|
|
1427
|
+
kids = " ".join(f"{pid} 0 R" for pid in page_ids)
|
|
1428
|
+
pages_content = f"<< /Type /Pages /Kids [{kids}] /Count {len(page_ids)} >>"
|
|
1429
|
+
pages_bytes = f"2 0 obj\n{pages_content}\nendobj\n".encode("latin-1")
|
|
1430
|
+
old_pages = objects[1]
|
|
1431
|
+
buf = buf.replace(b"2 0 obj\nPAGES_PLACEHOLDER\nendobj\n", pages_bytes)
|
|
1432
|
+
size_diff = len(pages_bytes) - len(old_pages)
|
|
1433
|
+
for i in range(2, len(offsets)):
|
|
1434
|
+
offsets[i] += size_diff
|
|
1435
|
+
|
|
1436
|
+
xref_offset = len(buf)
|
|
1437
|
+
buf += b"xref\n"
|
|
1438
|
+
buf += f"0 {len(objects) + 1}\n".encode("latin-1")
|
|
1439
|
+
buf += b"0000000000 65535 f \n"
|
|
1440
|
+
for off in offsets:
|
|
1441
|
+
buf += f"{off:010d} 00000 n \n".encode("latin-1")
|
|
1442
|
+
buf += b"trailer\n"
|
|
1443
|
+
buf += f"<< /Size {len(objects) + 1} /Root {catalog_id} 0 R >>\n".encode("latin-1")
|
|
1444
|
+
buf += b"startxref\n"
|
|
1445
|
+
buf += f"{xref_offset}\n".encode("latin-1")
|
|
1446
|
+
buf += b"%%EOF\n"
|
|
1447
|
+
return buf
|
|
1448
|
+
|
|
1313
1449
|
def compress_text_blob(text: str) -> str:
|
|
1314
1450
|
src = str(text or "")
|
|
1315
1451
|
if not src:
|
|
@@ -3435,6 +3571,7 @@ def _module_exists(name: str) -> bool:
|
|
|
3435
3571
|
|
|
3436
3572
|
def detect_upload_parser_capabilities() -> dict:
|
|
3437
3573
|
return {
|
|
3574
|
+
"pdfminer": _module_exists("pdfminer"),
|
|
3438
3575
|
"openpyxl": _module_exists("openpyxl"),
|
|
3439
3576
|
"xlrd": _module_exists("xlrd"),
|
|
3440
3577
|
"python_docx": _module_exists("docx"),
|
|
@@ -3446,6 +3583,7 @@ def detect_upload_parser_capabilities() -> dict:
|
|
|
3446
3583
|
"catdoc": bool(shutil.which("catdoc")),
|
|
3447
3584
|
"catppt": bool(shutil.which("catppt")),
|
|
3448
3585
|
"textutil": bool(shutil.which("textutil")),
|
|
3586
|
+
"playwright": _module_exists("playwright"),
|
|
3449
3587
|
}
|
|
3450
3588
|
|
|
3451
3589
|
def _render_cap_markdown(caps: dict) -> str:
|
|
@@ -4671,6 +4809,96 @@ SKILL_PROTOCOL_SPECS = [
|
|
|
4671
4809
|
},
|
|
4672
4810
|
]
|
|
4673
4811
|
|
|
4812
|
+
# ---------------------------------------------------------------------------
|
|
4813
|
+
# Built-in skill guides (injected into SkillStore on reload)
|
|
4814
|
+
# ---------------------------------------------------------------------------
|
|
4815
|
+
_BUILTIN_SKILLS: dict[str, dict] = {
|
|
4816
|
+
"workspace-paths": {
|
|
4817
|
+
"description": "File path rules, virtual path mapping, relative path conventions",
|
|
4818
|
+
"body": (
|
|
4819
|
+
"# Workspace Paths Guide\n"
|
|
4820
|
+
"- Always use relative paths (e.g. src/main.py) for file tools.\n"
|
|
4821
|
+
"- Runtime maps relative paths to session absolute root automatically.\n"
|
|
4822
|
+
"- '/workspace/...' is a virtual alias for tool arguments only; never create OS-level /workspace.\n"
|
|
4823
|
+
"- When user references uploaded files, prioritize workspace uploads directory.\n"
|
|
4824
|
+
"- For shell commands, use relative paths or $PWD-based references.\n"
|
|
4825
|
+
),
|
|
4826
|
+
},
|
|
4827
|
+
"task-management": {
|
|
4828
|
+
"description": "Todo/Task creation, updates, and best practices",
|
|
4829
|
+
"body": (
|
|
4830
|
+
"# Task Management Guide\n"
|
|
4831
|
+
"- For level 1-2 (simple) tasks: skip todo scaffolding, give direct response.\n"
|
|
4832
|
+
"- For level 3+ tasks: call TodoWrite early with 3-7 concise items, one marked in_progress.\n"
|
|
4833
|
+
"- Update todos only when plan or status actually changes. Avoid redundant calls.\n"
|
|
4834
|
+
"- If TodoWrite fails or repeats unchanged, use TodoWriteRescue with simple string items.\n"
|
|
4835
|
+
"- Use task_create/task_update/task_list for multi-step structured work.\n"
|
|
4836
|
+
),
|
|
4837
|
+
},
|
|
4838
|
+
"tool-best-practices": {
|
|
4839
|
+
"description": "write_file vs edit_file, bash usage, error handling conventions",
|
|
4840
|
+
"body": (
|
|
4841
|
+
"# Tool Best Practices\n"
|
|
4842
|
+
"- Prefer write_file/edit_file for code changes (UI renders line-level diffs).\n"
|
|
4843
|
+
"- If write_file/edit_file fails due to malformed arguments, regenerate complete JSON.\n"
|
|
4844
|
+
"- If output looks truncated, split into smaller subtasks.\n"
|
|
4845
|
+
"- For shell commands: use bash_command for execution, not file tools.\n"
|
|
4846
|
+
"- Always end thinking sections with either a final answer or one tool call.\n"
|
|
4847
|
+
"- Never stop at thinking-only content without an action.\n"
|
|
4848
|
+
),
|
|
4849
|
+
},
|
|
4850
|
+
"context-management": {
|
|
4851
|
+
"description": "Context compaction triggers, context_recall usage, step sizing",
|
|
4852
|
+
"body": (
|
|
4853
|
+
"# Context Management Guide\n"
|
|
4854
|
+
"- Context has a token upper bound; keep steps compact.\n"
|
|
4855
|
+
"- When <compact-resume> hint appears, inherit pending todos and continue immediately.\n"
|
|
4856
|
+
"- After compaction, use context_recall to fetch archived messages by segment_id/query.\n"
|
|
4857
|
+
"- Do not guess content that was compacted away—recall it first.\n"
|
|
4858
|
+
"- For large tasks, break into subtasks to avoid context overflow.\n"
|
|
4859
|
+
),
|
|
4860
|
+
},
|
|
4861
|
+
"multi-agent-guide": {
|
|
4862
|
+
"description": "Blackboard read/write norms, ask_colleague usage, handoff format",
|
|
4863
|
+
"body": (
|
|
4864
|
+
"# Multi-Agent Guide\n"
|
|
4865
|
+
"- Use read_from_blackboard to check shared state before acting.\n"
|
|
4866
|
+
"- Use write_to_blackboard to record progress, artifacts, and blockers.\n"
|
|
4867
|
+
"- Use ask_colleague with structured intent for inter-agent communication:\n"
|
|
4868
|
+
" - explorer->developer: 'handoff' with research findings\n"
|
|
4869
|
+
" - developer->reviewer: 'review_request' with changed files list\n"
|
|
4870
|
+
" - reviewer->developer: 'fix_request' with concrete failure evidence\n"
|
|
4871
|
+
"- Handoffs should include enough context for the target agent to act independently.\n"
|
|
4872
|
+
"- Keep blackboard updates atomic and concise.\n"
|
|
4873
|
+
),
|
|
4874
|
+
},
|
|
4875
|
+
"code-review-checklist": {
|
|
4876
|
+
"description": "Reviewer role checklist for code verification",
|
|
4877
|
+
"body": (
|
|
4878
|
+
"# Code Review Checklist\n"
|
|
4879
|
+
"1. Does the implementation match the original goal?\n"
|
|
4880
|
+
"2. Are there syntax errors? Run linting/type checks if available.\n"
|
|
4881
|
+
"3. Are there obvious logic bugs or edge cases missed?\n"
|
|
4882
|
+
"4. Do tests pass? If no tests exist, verify manually.\n"
|
|
4883
|
+
"5. Are there security issues (injection, XSS, hardcoded secrets)?\n"
|
|
4884
|
+
"6. Write review_feedback to blackboard with pass/fail and evidence.\n"
|
|
4885
|
+
"7. If fail: send fix_request via ask_colleague with specific issues.\n"
|
|
4886
|
+
),
|
|
4887
|
+
},
|
|
4888
|
+
"finish-protocol": {
|
|
4889
|
+
"description": "When and how to call finish_current_task correctly",
|
|
4890
|
+
"body": (
|
|
4891
|
+
"# Finish Protocol\n"
|
|
4892
|
+
"- Call finish_current_task when all required work is complete.\n"
|
|
4893
|
+
"- Include a concise summary of what was done.\n"
|
|
4894
|
+
"- For multi-agent mode: finish triggers auto-summary from blackboard state.\n"
|
|
4895
|
+
"- Do not finish if there are known failing tests or unresolved blockers.\n"
|
|
4896
|
+
"- If todos have stale pending items but work is done, finish anyway—stale items are cleared automatically.\n"
|
|
4897
|
+
"- Summary format: list modified files, key changes, and validation status.\n"
|
|
4898
|
+
),
|
|
4899
|
+
},
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4674
4902
|
class SkillStore:
|
|
4675
4903
|
def __init__(self, skills_root: Path):
|
|
4676
4904
|
self.skills_root = skills_root
|
|
@@ -5075,9 +5303,34 @@ class SkillStore:
|
|
|
5075
5303
|
self._load_local_skills()
|
|
5076
5304
|
self._load_manifest_providers()
|
|
5077
5305
|
self._load_clawhub_autodetect()
|
|
5306
|
+
self._inject_builtin_skills()
|
|
5078
5307
|
self.fingerprint = fp
|
|
5079
5308
|
self.last_reload_ts = now
|
|
5080
5309
|
|
|
5310
|
+
def _inject_builtin_skills(self):
|
|
5311
|
+
"""Register built-in skill guides for small-model support."""
|
|
5312
|
+
provider_id = "builtin"
|
|
5313
|
+
if not self._provider_exists(provider_id):
|
|
5314
|
+
self._register_provider(
|
|
5315
|
+
provider_id, "builtin", "1.0",
|
|
5316
|
+
"Built-in skill guides for agent operation",
|
|
5317
|
+
)
|
|
5318
|
+
for skill_name, data in _BUILTIN_SKILLS.items():
|
|
5319
|
+
key = f"{provider_id}:{skill_name}"
|
|
5320
|
+
if key in self.skills:
|
|
5321
|
+
continue
|
|
5322
|
+
self._register_skill(
|
|
5323
|
+
provider_id=provider_id,
|
|
5324
|
+
protocol="builtin",
|
|
5325
|
+
protocol_version="1.0",
|
|
5326
|
+
name=skill_name,
|
|
5327
|
+
description=data["description"],
|
|
5328
|
+
meta={"builtin": True},
|
|
5329
|
+
body=data["body"],
|
|
5330
|
+
skill_path="(builtin)",
|
|
5331
|
+
attachments=[],
|
|
5332
|
+
)
|
|
5333
|
+
|
|
5081
5334
|
def descriptions(self, max_items: int = SKILL_PROMPT_MAX_ITEMS, max_chars: int = SKILL_PROMPT_MAX_CHARS) -> str:
|
|
5082
5335
|
if not self.skills:
|
|
5083
5336
|
return "(no skills)"
|
|
@@ -6486,11 +6739,14 @@ class OllamaClient:
|
|
|
6486
6739
|
think: bool = False,
|
|
6487
6740
|
on_thinking_chunk=None,
|
|
6488
6741
|
) -> dict:
|
|
6742
|
+
effective_max = max_tokens
|
|
6743
|
+
if tools:
|
|
6744
|
+
effective_max = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
|
|
6489
6745
|
payload = {
|
|
6490
6746
|
"model": self.model,
|
|
6491
6747
|
"messages": req_messages,
|
|
6492
6748
|
"stream": True,
|
|
6493
|
-
"options": {"temperature": temperature, "num_predict":
|
|
6749
|
+
"options": {"temperature": temperature, "num_predict": effective_max},
|
|
6494
6750
|
}
|
|
6495
6751
|
if tools:
|
|
6496
6752
|
payload["tools"] = tools
|
|
@@ -6503,6 +6759,7 @@ class OllamaClient:
|
|
|
6503
6759
|
full_content: list[str] = []
|
|
6504
6760
|
full_thinking: list[str] = []
|
|
6505
6761
|
tool_calls: list[dict] = []
|
|
6762
|
+
done_reason = ""
|
|
6506
6763
|
try:
|
|
6507
6764
|
with urlopen(req, timeout=self.timeout) as resp:
|
|
6508
6765
|
while True:
|
|
@@ -6542,6 +6799,7 @@ class OllamaClient:
|
|
|
6542
6799
|
if tcs:
|
|
6543
6800
|
tool_calls = self._normalize_tool_calls(tcs)
|
|
6544
6801
|
if part.get("done"):
|
|
6802
|
+
done_reason = str(part.get("done_reason") or "").strip().lower()
|
|
6545
6803
|
break
|
|
6546
6804
|
except HTTPError as exc:
|
|
6547
6805
|
text = exc.read().decode("utf-8", errors="replace")
|
|
@@ -6554,7 +6812,7 @@ class OllamaClient:
|
|
|
6554
6812
|
"content": content,
|
|
6555
6813
|
"thinking": thinking_content,
|
|
6556
6814
|
"tool_calls": tool_calls,
|
|
6557
|
-
"raw": {"streamed": True},
|
|
6815
|
+
"raw": {"streamed": True, "done_reason": done_reason},
|
|
6558
6816
|
}
|
|
6559
6817
|
|
|
6560
6818
|
def chat(
|
|
@@ -6618,11 +6876,14 @@ class OllamaClient:
|
|
|
6618
6876
|
fallback_status = {0, 400, 404, 405, 500, 501, 502, 503, 504}
|
|
6619
6877
|
if status not in fallback_status:
|
|
6620
6878
|
raise
|
|
6879
|
+
effective_max_native = max_tokens
|
|
6880
|
+
if tools:
|
|
6881
|
+
effective_max_native = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
|
|
6621
6882
|
native_payload = {
|
|
6622
6883
|
"model": self.model,
|
|
6623
6884
|
"messages": req_messages,
|
|
6624
6885
|
"stream": False,
|
|
6625
|
-
"options": {"temperature": temperature, "num_predict":
|
|
6886
|
+
"options": {"temperature": temperature, "num_predict": effective_max_native},
|
|
6626
6887
|
}
|
|
6627
6888
|
if tools:
|
|
6628
6889
|
native_payload["tools"] = tools
|
|
@@ -6918,6 +7179,7 @@ class SessionState:
|
|
|
6918
7179
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
6919
7180
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
6920
7181
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
7182
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
6921
7183
|
ui_language: str = DEFAULT_UI_LANGUAGE,
|
|
6922
7184
|
js_lib_root: Path | None = None,
|
|
6923
7185
|
owner_user_id: str = "",
|
|
@@ -6973,8 +7235,9 @@ class SessionState:
|
|
|
6973
7235
|
self.bus = MessageBus(self.root / "team" / "inbox", crypto)
|
|
6974
7236
|
self.worktrees = WorktreeManager(self.id, self.tasks, self.root, crypto, repo_root)
|
|
6975
7237
|
self.messages: list[dict] = []
|
|
6976
|
-
self.
|
|
6977
|
-
self.
|
|
7238
|
+
self.agent_messages: list[dict] = [] # unified agent context (replaces contexts + manager_context)
|
|
7239
|
+
self.contexts: dict[str, list[dict]] = {role: [] for role in AGENT_ROLES} # kept as view caches
|
|
7240
|
+
self.manager_context: list[dict] = [] # kept as view cache
|
|
6978
7241
|
self.blackboard: dict = {}
|
|
6979
7242
|
self.agent_bus_messages: list[dict] = []
|
|
6980
7243
|
self.manager_routes: list[dict] = []
|
|
@@ -7055,6 +7318,7 @@ class SessionState:
|
|
|
7055
7318
|
MIN_AGENT_ROUNDS,
|
|
7056
7319
|
min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
|
|
7057
7320
|
)
|
|
7321
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
7058
7322
|
self.max_run_seconds = normalize_timeout_seconds(
|
|
7059
7323
|
max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
|
|
7060
7324
|
minimum=MIN_RUN_TIMEOUT_SECONDS,
|
|
@@ -8037,6 +8301,13 @@ class SessionState:
|
|
|
8037
8301
|
if isinstance(row, dict):
|
|
8038
8302
|
clean_manager_context.append(dict(row))
|
|
8039
8303
|
self.manager_context = clean_manager_context[-400:]
|
|
8304
|
+
raw_agent_messages = raw.get("agent_messages", [])
|
|
8305
|
+
if isinstance(raw_agent_messages, list):
|
|
8306
|
+
clean_am: list[dict] = []
|
|
8307
|
+
for row in raw_agent_messages[-1200:]:
|
|
8308
|
+
if isinstance(row, dict):
|
|
8309
|
+
clean_am.append(dict(row))
|
|
8310
|
+
self.agent_messages = clean_am[-800:]
|
|
8040
8311
|
raw_blackboard = raw.get("blackboard", {})
|
|
8041
8312
|
self.blackboard = self._normalize_blackboard(raw_blackboard)
|
|
8042
8313
|
raw_bus = raw.get("agent_bus_messages", [])
|
|
@@ -8159,6 +8430,7 @@ class SessionState:
|
|
|
8159
8430
|
"active_agent_role": str(self.active_agent_role or ""),
|
|
8160
8431
|
"contexts": {role: list(self.contexts.get(role, []))[-400:] for role in AGENT_ROLES},
|
|
8161
8432
|
"manager_context": list(self.manager_context)[-400:],
|
|
8433
|
+
"agent_messages": list(self.agent_messages)[-800:],
|
|
8162
8434
|
"blackboard": self._normalize_blackboard(self.blackboard),
|
|
8163
8435
|
"agent_bus_messages": list(self.agent_bus_messages)[-240:],
|
|
8164
8436
|
"manager_routes": list(self.manager_routes)[-240:],
|
|
@@ -8625,75 +8897,25 @@ class SessionState:
|
|
|
8625
8897
|
research_hint = self._deep_research_boost_instruction()
|
|
8626
8898
|
runtime_level = int(self.runtime_task_level or 0)
|
|
8627
8899
|
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
|
-
)
|
|
8900
|
+
budget = int(self.runtime_round_budget or 0)
|
|
8661
8901
|
html_block = f"{html_hint}\n\n" if html_hint else ""
|
|
8662
8902
|
research_block = f"{research_hint}\n\n" if research_hint else ""
|
|
8663
8903
|
return (
|
|
8664
|
-
f"You are a coding agent
|
|
8665
|
-
f"
|
|
8666
|
-
"
|
|
8667
|
-
"
|
|
8904
|
+
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
8905
|
+
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
8906
|
+
f"budget={'unlimited' if budget <= 0 else budget}. "
|
|
8907
|
+
f"Context limit ~{self.context_token_upper_bound} tokens. "
|
|
8668
8908
|
f"{_detect_os_shell_instruction()} "
|
|
8669
|
-
"Use tools to inspect
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
"
|
|
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"
|
|
8909
|
+
"Use tools to inspect, edit, and execute. "
|
|
8910
|
+
"Call finish_current_task when done. "
|
|
8911
|
+
"Use load_skill for guidance on specific topics "
|
|
8912
|
+
"(workspace-paths, task-management, tool-best-practices, context-management). "
|
|
8913
|
+
"If execution stalls, load_skill('execution-degradation-recovery'). "
|
|
8692
8914
|
f"{html_block}"
|
|
8693
8915
|
f"{research_block}"
|
|
8694
8916
|
f"{model_language_instruction(self.ui_language)}\n\n"
|
|
8695
|
-
f"
|
|
8696
|
-
f"
|
|
8917
|
+
f"Uploads:\n{uploads_ctx}\n\n"
|
|
8918
|
+
f"Skills:\n{self.skills.descriptions()}"
|
|
8697
8919
|
)
|
|
8698
8920
|
|
|
8699
8921
|
def _estimate_tokens(self) -> int:
|
|
@@ -9261,7 +9483,7 @@ class SessionState:
|
|
|
9261
9483
|
if tool_calls:
|
|
9262
9484
|
return False
|
|
9263
9485
|
|
|
9264
|
-
near_limit = output_tokens >= int(
|
|
9486
|
+
near_limit = output_tokens >= int(self.max_output_tokens * 0.90)
|
|
9265
9487
|
# Mid-size outputs (e.g. planning text ending with a Chinese colon) should not be
|
|
9266
9488
|
# treated as truncation unless close to max tokens or JSON-like unfinished payload.
|
|
9267
9489
|
json_like_tail = bool(
|
|
@@ -9406,17 +9628,108 @@ class SessionState:
|
|
|
9406
9628
|
task_ids = self._create_truncation_subtasks(reason)
|
|
9407
9629
|
self._inject_truncation_rescue_hint(reason, output_tokens, task_ids)
|
|
9408
9630
|
|
|
9409
|
-
def
|
|
9631
|
+
def _find_tool_name_by_id(self, messages: list[dict], tool_use_id: str) -> str:
|
|
9632
|
+
"""Find tool name from preceding assistant message by tool_use_id."""
|
|
9633
|
+
if not tool_use_id:
|
|
9634
|
+
return ""
|
|
9635
|
+
for msg in reversed(messages):
|
|
9636
|
+
if msg.get("role") != "assistant":
|
|
9637
|
+
continue
|
|
9638
|
+
for tc in msg.get("tool_calls", []):
|
|
9639
|
+
if isinstance(tc, dict) and tc.get("id") == tool_use_id:
|
|
9640
|
+
fn = tc.get("function", {})
|
|
9641
|
+
return str(fn.get("name", "")) if isinstance(fn, dict) else ""
|
|
9642
|
+
content = msg.get("content")
|
|
9643
|
+
if isinstance(content, list):
|
|
9644
|
+
for block in content:
|
|
9645
|
+
if isinstance(block, dict) and block.get("type") == "tool_use" and block.get("id") == tool_use_id:
|
|
9646
|
+
return str(block.get("name", ""))
|
|
9647
|
+
return ""
|
|
9648
|
+
|
|
9649
|
+
def _microcompact(self, keep_recent: int = 3):
|
|
9650
|
+
"""Replace old tool results with compact placeholders to save tokens.
|
|
9651
|
+
|
|
9652
|
+
Handles both OpenAI-style (role=tool) and Anthropic-style (tool_result blocks)
|
|
9653
|
+
messages. Keeps the most recent `keep_recent` tool interactions intact.
|
|
9654
|
+
"""
|
|
9655
|
+
# Phase 1: OpenAI-style role=tool messages
|
|
9410
9656
|
tool_messages = [m for m in self.messages if m.get("role") == "tool"]
|
|
9411
|
-
if len(tool_messages)
|
|
9657
|
+
if len(tool_messages) > keep_recent:
|
|
9658
|
+
kept = tool_messages[-keep_recent:]
|
|
9659
|
+
keep_ids = {id(x) for x in kept}
|
|
9660
|
+
for msg in self.messages:
|
|
9661
|
+
if msg.get("role") == "tool" and id(msg) not in keep_ids:
|
|
9662
|
+
content = msg.get("content", "")
|
|
9663
|
+
if isinstance(content, str) and len(content) > 120:
|
|
9664
|
+
msg["content"] = "[cleared by microcompact]"
|
|
9665
|
+
|
|
9666
|
+
# Phase 2: Anthropic-style tool_result blocks in user messages
|
|
9667
|
+
assistant_indices = [i for i, m in enumerate(self.messages) if m.get("role") == "assistant"]
|
|
9668
|
+
if len(assistant_indices) <= keep_recent:
|
|
9412
9669
|
return
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
if msg.get("role") == "
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9670
|
+
cutoff = assistant_indices[-keep_recent]
|
|
9671
|
+
for i in range(cutoff):
|
|
9672
|
+
msg = self.messages[i]
|
|
9673
|
+
if msg.get("role") == "user" and isinstance(msg.get("content"), list):
|
|
9674
|
+
changed = False
|
|
9675
|
+
new_content = []
|
|
9676
|
+
for block in msg["content"]:
|
|
9677
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
9678
|
+
block_content = block.get("content", "")
|
|
9679
|
+
if isinstance(block_content, str) and len(block_content) > 120:
|
|
9680
|
+
tool_id = block.get("tool_use_id", "")
|
|
9681
|
+
tool_name = self._find_tool_name_by_id(self.messages[:i], tool_id) or "tool"
|
|
9682
|
+
new_content.append({
|
|
9683
|
+
"type": "tool_result",
|
|
9684
|
+
"tool_use_id": tool_id,
|
|
9685
|
+
"content": f"[Previous: used {tool_name}]",
|
|
9686
|
+
})
|
|
9687
|
+
changed = True
|
|
9688
|
+
else:
|
|
9689
|
+
new_content.append(block)
|
|
9690
|
+
else:
|
|
9691
|
+
new_content.append(block)
|
|
9692
|
+
if changed:
|
|
9693
|
+
msg["content"] = new_content
|
|
9694
|
+
|
|
9695
|
+
def _microcompact_agent_messages(self, messages: list[dict], keep_recent: int = 3):
|
|
9696
|
+
"""Apply microcompact to an agent-specific message list (e.g. agent_messages filtered by role)."""
|
|
9697
|
+
tool_messages = [m for m in messages if m.get("role") == "tool"]
|
|
9698
|
+
if len(tool_messages) > keep_recent:
|
|
9699
|
+
kept = tool_messages[-keep_recent:]
|
|
9700
|
+
keep_ids = {id(x) for x in kept}
|
|
9701
|
+
for msg in messages:
|
|
9702
|
+
if msg.get("role") == "tool" and id(msg) not in keep_ids:
|
|
9703
|
+
content = msg.get("content", "")
|
|
9704
|
+
if isinstance(content, str) and len(content) > 120:
|
|
9705
|
+
msg["content"] = "[cleared by microcompact]"
|
|
9706
|
+
assistant_indices = [i for i, m in enumerate(messages) if m.get("role") == "assistant"]
|
|
9707
|
+
if len(assistant_indices) <= keep_recent:
|
|
9708
|
+
return
|
|
9709
|
+
cutoff = assistant_indices[-keep_recent]
|
|
9710
|
+
for i in range(cutoff):
|
|
9711
|
+
msg = messages[i]
|
|
9712
|
+
if msg.get("role") == "user" and isinstance(msg.get("content"), list):
|
|
9713
|
+
changed = False
|
|
9714
|
+
new_content = []
|
|
9715
|
+
for block in msg["content"]:
|
|
9716
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
9717
|
+
block_content = block.get("content", "")
|
|
9718
|
+
if isinstance(block_content, str) and len(block_content) > 120:
|
|
9719
|
+
tool_id = block.get("tool_use_id", "")
|
|
9720
|
+
tool_name = self._find_tool_name_by_id(messages[:i], tool_id) or "tool"
|
|
9721
|
+
new_content.append({
|
|
9722
|
+
"type": "tool_result",
|
|
9723
|
+
"tool_use_id": tool_id,
|
|
9724
|
+
"content": f"[Previous: used {tool_name}]",
|
|
9725
|
+
})
|
|
9726
|
+
changed = True
|
|
9727
|
+
else:
|
|
9728
|
+
new_content.append(block)
|
|
9729
|
+
else:
|
|
9730
|
+
new_content.append(block)
|
|
9731
|
+
if changed:
|
|
9732
|
+
msg["content"] = new_content
|
|
9420
9733
|
|
|
9421
9734
|
def _estimate_messages_tokens(self, rows: list[dict]) -> int:
|
|
9422
9735
|
try:
|
|
@@ -10090,6 +10403,17 @@ class SessionState:
|
|
|
10090
10403
|
return data.decode("latin-1", errors="ignore")
|
|
10091
10404
|
|
|
10092
10405
|
def _extract_pdf_text(self, pdf_path: Path) -> str:
|
|
10406
|
+
# 优先使用 pdfminer.six(纯 Python,无外部依赖)
|
|
10407
|
+
try:
|
|
10408
|
+
from pdfminer.high_level import extract_text
|
|
10409
|
+
text = extract_text(str(pdf_path))
|
|
10410
|
+
if text and text.strip():
|
|
10411
|
+
return text.strip()
|
|
10412
|
+
except ImportError:
|
|
10413
|
+
pass
|
|
10414
|
+
except Exception:
|
|
10415
|
+
pass
|
|
10416
|
+
# 降级:pdftotext CLI
|
|
10093
10417
|
tool = shutil.which("pdftotext")
|
|
10094
10418
|
if tool:
|
|
10095
10419
|
try:
|
|
@@ -10103,6 +10427,7 @@ class SessionState:
|
|
|
10103
10427
|
return r.stdout.strip()
|
|
10104
10428
|
except Exception:
|
|
10105
10429
|
pass
|
|
10430
|
+
# 最终降级:regex 提取
|
|
10106
10431
|
try:
|
|
10107
10432
|
raw = pdf_path.read_bytes()
|
|
10108
10433
|
text = raw.decode("latin-1", errors="ignore")
|
|
@@ -10435,6 +10760,16 @@ class SessionState:
|
|
|
10435
10760
|
lines.append(chunk)
|
|
10436
10761
|
lines.append("</uploaded_excerpt>")
|
|
10437
10762
|
remaining -= len(chunk)
|
|
10763
|
+
# 提示模型可直接读取 .parsed.md 文件获取完整解析文本
|
|
10764
|
+
item_kind = item.get("kind", "file")
|
|
10765
|
+
if item_kind not in ("text", "code"):
|
|
10766
|
+
wp = item.get("workspace_path", "")
|
|
10767
|
+
if wp:
|
|
10768
|
+
from pathlib import PurePosixPath
|
|
10769
|
+
stem = PurePosixPath(wp).stem
|
|
10770
|
+
parent = str(PurePosixPath(wp).parent)
|
|
10771
|
+
parsed_rel = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
|
|
10772
|
+
lines.append(f" (parsed text available at: {parsed_rel} — use read_file to access full content)")
|
|
10438
10773
|
return "\n".join(lines)
|
|
10439
10774
|
|
|
10440
10775
|
def add_upload(self, filename: str, raw: bytes, mime: str = "") -> dict:
|
|
@@ -10445,10 +10780,42 @@ class SessionState:
|
|
|
10445
10780
|
stored.write_bytes(raw)
|
|
10446
10781
|
ext = stored.suffix.lower()
|
|
10447
10782
|
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
|
-
".
|
|
10783
|
+
".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl",
|
|
10784
|
+
".go", ".rs", ".rb", ".php", ".swift", ".kt", ".kts", ".scala", ".sh", ".bash", ".zsh", ".fish", ".sql", ".html", ".htm",
|
|
10785
|
+
".css", ".sass", ".scss", ".less", ".styl", ".json", ".jsonc", ".yaml", ".yml", ".xml", ".xsd", ".xsl",
|
|
10786
|
+
".toml", ".ini", ".cfg", ".conf", ".env", ".properties", ".md", ".mdx", ".txt", ".rst", ".log",
|
|
10787
|
+
".ipynb", ".vue", ".svelte", ".cs", ".m", ".mm", ".r", ".pl", ".pm", ".csv", ".tsv",
|
|
10788
|
+
# Fortran
|
|
10789
|
+
".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
|
|
10790
|
+
# 更多语言
|
|
10791
|
+
".zig", ".nim", ".v", ".d", ".ada", ".adb", ".ads",
|
|
10792
|
+
".asm", ".s",
|
|
10793
|
+
".bas", ".vb", ".vbs", ".vba",
|
|
10794
|
+
".bat", ".cmd", ".ps1", ".psm1",
|
|
10795
|
+
".clj", ".cljs", ".cljc", ".edn",
|
|
10796
|
+
".coffee", ".cr", ".dart",
|
|
10797
|
+
".dockerfile",
|
|
10798
|
+
".erl", ".hrl", ".ex", ".exs",
|
|
10799
|
+
".fs", ".fsi", ".fsx",
|
|
10800
|
+
".gradle", ".groovy", ".gvy",
|
|
10801
|
+
".hs", ".lhs",
|
|
10802
|
+
".jl", ".lua",
|
|
10803
|
+
".lisp", ".cl", ".el", ".scm", ".rkt",
|
|
10804
|
+
".mk", ".cmake",
|
|
10805
|
+
".ml", ".mli", ".nix",
|
|
10806
|
+
".pas", ".pp", ".inc",
|
|
10807
|
+
".pde", ".ino",
|
|
10808
|
+
".proto", ".purs",
|
|
10809
|
+
".raku", ".p6",
|
|
10810
|
+
".sol",
|
|
10811
|
+
".sv", ".svh", ".vh", ".vhd", ".vhdl",
|
|
10812
|
+
".tcl", ".tk",
|
|
10813
|
+
".tf", ".tfvars", ".hcl",
|
|
10814
|
+
".tex", ".bib", ".sty", ".cls", ".typ",
|
|
10815
|
+
".wat",
|
|
10816
|
+
".diff", ".patch",
|
|
10817
|
+
".graphql", ".gql",
|
|
10818
|
+
".prisma", ".svg",
|
|
10452
10819
|
}
|
|
10453
10820
|
parsed_excerpt = ""
|
|
10454
10821
|
kind = "binary"
|
|
@@ -10489,6 +10856,15 @@ class SessionState:
|
|
|
10489
10856
|
workspace_target = self._upload_workspace_target(safe_name)
|
|
10490
10857
|
workspace_target.parent.mkdir(parents=True, exist_ok=True)
|
|
10491
10858
|
workspace_target.write_bytes(raw)
|
|
10859
|
+
# 当 parsed_excerpt 非空且原始文件不是纯文本时,保存解析结果为 .parsed.md
|
|
10860
|
+
parsed_target: Path | None = None
|
|
10861
|
+
if parsed_excerpt and kind not in ("text", "code"):
|
|
10862
|
+
parsed_target = workspace_target.parent / f"{workspace_target.stem}.parsed.md"
|
|
10863
|
+
try:
|
|
10864
|
+
header = f"# {safe_name}\n\n> Auto-parsed from uploaded {kind} file ({len(raw)} bytes)\n\n"
|
|
10865
|
+
parsed_target.write_text(header + parsed_excerpt, encoding="utf-8")
|
|
10866
|
+
except Exception:
|
|
10867
|
+
parsed_target = None # 解析文件保存失败不影响主流程
|
|
10492
10868
|
workspace_rel = self._session_rel(workspace_target)
|
|
10493
10869
|
meta = {
|
|
10494
10870
|
"id": upload_id,
|
|
@@ -10507,6 +10883,9 @@ class SessionState:
|
|
|
10507
10883
|
self.uploads = self.uploads[-80:]
|
|
10508
10884
|
self.updated_at = now_ts()
|
|
10509
10885
|
self._persist()
|
|
10886
|
+
if parsed_excerpt:
|
|
10887
|
+
bb_content = f"[upload:{safe_name}]\n{trim(parsed_excerpt, BLACKBOARD_MAX_TEXT - 200)}"
|
|
10888
|
+
self._blackboard_append_section("research_notes", "system", bb_content)
|
|
10510
10889
|
self._emit(
|
|
10511
10890
|
"upload",
|
|
10512
10891
|
{
|
|
@@ -10514,7 +10893,10 @@ class SessionState:
|
|
|
10514
10893
|
"workspace_path": workspace_rel,
|
|
10515
10894
|
"kind": kind,
|
|
10516
10895
|
"size": len(raw),
|
|
10517
|
-
"summary":
|
|
10896
|
+
"summary": (
|
|
10897
|
+
f"upload: {safe_name} -> {workspace_rel}"
|
|
10898
|
+
+ (f" (parsed text: {self._session_rel(parsed_target)})" if parsed_target else "")
|
|
10899
|
+
),
|
|
10518
10900
|
"preview": trim(parsed_excerpt, 500),
|
|
10519
10901
|
},
|
|
10520
10902
|
)
|
|
@@ -10611,6 +10993,12 @@ class SessionState:
|
|
|
10611
10993
|
"completed",
|
|
10612
10994
|
"finished",
|
|
10613
10995
|
"all set",
|
|
10996
|
+
# 明确表示拒绝/无法完成也应视为终结
|
|
10997
|
+
"抱歉",
|
|
10998
|
+
"sorry",
|
|
10999
|
+
"无法",
|
|
11000
|
+
"cannot",
|
|
11001
|
+
"unable",
|
|
10614
11002
|
]
|
|
10615
11003
|
if any(x in t for x in done_markers):
|
|
10616
11004
|
return False
|
|
@@ -10692,6 +11080,17 @@ class SessionState:
|
|
|
10692
11080
|
"that's all",
|
|
10693
11081
|
"that is all",
|
|
10694
11082
|
"as requested",
|
|
11083
|
+
# 明确表示无法完成的标记
|
|
11084
|
+
"抱歉,我无法",
|
|
11085
|
+
"无法直接获取",
|
|
11086
|
+
"无法完成",
|
|
11087
|
+
"cannot be done",
|
|
11088
|
+
"unable to",
|
|
11089
|
+
"not possible",
|
|
11090
|
+
"建议您通过",
|
|
11091
|
+
"建议你通过",
|
|
11092
|
+
"i cannot",
|
|
11093
|
+
"i'm unable",
|
|
10695
11094
|
]
|
|
10696
11095
|
return any(x in t for x in done_markers)
|
|
10697
11096
|
|
|
@@ -11098,7 +11497,7 @@ class SessionState:
|
|
|
11098
11497
|
return False
|
|
11099
11498
|
if not str(thinking_text or "").strip():
|
|
11100
11499
|
return False
|
|
11101
|
-
threshold = int(max(1,
|
|
11500
|
+
threshold = int(max(1, self.max_output_tokens) * float(THINKING_BUDGET_FORCE_RATIO))
|
|
11102
11501
|
return int(output_tokens or 0) >= max(1, threshold)
|
|
11103
11502
|
|
|
11104
11503
|
def _is_thinking_only_dead_turn(self, text: str, thinking_text: str, tool_calls: list | None = None) -> bool:
|
|
@@ -12229,12 +12628,48 @@ class SessionState:
|
|
|
12229
12628
|
fp = self._session_path(rel)
|
|
12230
12629
|
content = fp.read_text(encoding="utf-8")
|
|
12231
12630
|
if old_text not in content:
|
|
12232
|
-
|
|
12631
|
+
diag = self._edit_mismatch_diagnostic(content, old_text)
|
|
12632
|
+
return f"Error: text not found in {rel}. {diag}"
|
|
12233
12633
|
fp.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
|
|
12234
12634
|
return f"Edited {rel}"
|
|
12235
12635
|
except Exception as exc:
|
|
12236
12636
|
return f"Error: {type(exc).__name__}: {exc}"
|
|
12237
12637
|
|
|
12638
|
+
def _edit_mismatch_diagnostic(self, content: str, old_text: str) -> str:
|
|
12639
|
+
"""为 edit_file 匹配失败提供诊断信息"""
|
|
12640
|
+
lines = content.splitlines()
|
|
12641
|
+
first_line = old_text.strip().splitlines()[0].strip() if old_text.strip() else ""
|
|
12642
|
+
if not first_line:
|
|
12643
|
+
return "The old_text is empty or whitespace-only."
|
|
12644
|
+
# 搜索 old_text 的第一行在文件中的位置
|
|
12645
|
+
matches = []
|
|
12646
|
+
for i, line in enumerate(lines, 1):
|
|
12647
|
+
if first_line in line:
|
|
12648
|
+
matches.append(i)
|
|
12649
|
+
if matches:
|
|
12650
|
+
loc = ", ".join(str(m) for m in matches[:5])
|
|
12651
|
+
return (
|
|
12652
|
+
f"The first line of old_text was found at line(s) {loc}, "
|
|
12653
|
+
f"but the full multi-line match failed. "
|
|
12654
|
+
f"Likely cause: whitespace or indentation mismatch. "
|
|
12655
|
+
f"Tip: use read_file to get the exact content, then copy it precisely."
|
|
12656
|
+
)
|
|
12657
|
+
# 尝试空白规范化匹配
|
|
12658
|
+
norm_first = " ".join(first_line.split())
|
|
12659
|
+
for i, line in enumerate(lines, 1):
|
|
12660
|
+
norm_line = " ".join(line.split())
|
|
12661
|
+
if norm_first and norm_first in norm_line:
|
|
12662
|
+
return (
|
|
12663
|
+
f"A whitespace-normalized partial match was found near line {i}. "
|
|
12664
|
+
f"The old_text likely has wrong indentation or extra/missing spaces. "
|
|
12665
|
+
f"Use read_file to get exact content."
|
|
12666
|
+
)
|
|
12667
|
+
return (
|
|
12668
|
+
f"No match found. The file has {len(lines)} lines. "
|
|
12669
|
+
f"The old_text first line '{first_line[:60]}' does not appear in the file. "
|
|
12670
|
+
f"The content may have changed since last read. Use read_file to refresh."
|
|
12671
|
+
)
|
|
12672
|
+
|
|
12238
12673
|
def _write_global_skill(self, args: dict) -> str:
|
|
12239
12674
|
rel_raw = str(args.get("path", "") or "").strip().replace("\\", "/")
|
|
12240
12675
|
if not rel_raw:
|
|
@@ -12861,15 +13296,13 @@ class SessionState:
|
|
|
12861
13296
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
12862
13297
|
if bool(self.runtime_goal_reset_pending):
|
|
12863
13298
|
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"
|
|
13299
|
+
# Project todo gate: coding tasks must pass compile + test
|
|
13300
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
13301
|
+
task_type = str(profile.get("task_type", "general") or "general")
|
|
13302
|
+
if task_type in ("simple_code", "engineering"):
|
|
13303
|
+
for todo in bb.get("project_todos", []):
|
|
13304
|
+
if todo.get("category") in ("compile_test", "min_test") and todo.get("status") != "completed":
|
|
13305
|
+
return False, f"project-todo-incomplete:{todo.get('category', '')}"
|
|
12873
13306
|
return True, "ok"
|
|
12874
13307
|
|
|
12875
13308
|
def _invalidate_stale_approval_if_needed(
|
|
@@ -13322,9 +13755,6 @@ class SessionState:
|
|
|
13322
13755
|
"decomposer_output": trim(raw_text, 2000),
|
|
13323
13756
|
}
|
|
13324
13757
|
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
13758
|
wd["intent_no_tool_streak"] = 0
|
|
13329
13759
|
wd["repeat_no_tool_streak"] = 0
|
|
13330
13760
|
board["watchdog"] = wd
|
|
@@ -13353,6 +13783,39 @@ class SessionState:
|
|
|
13353
13783
|
)
|
|
13354
13784
|
return True
|
|
13355
13785
|
|
|
13786
|
+
def _watchdog_escalate_to_single_developer(self, board: dict, *, reason: str = ""):
|
|
13787
|
+
"""Watchdog 连续 stall 升级:强制降级到 Single+Developer 模式。"""
|
|
13788
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13789
|
+
self.runtime_execution_mode = EXECUTION_MODE_SINGLE
|
|
13790
|
+
self.runtime_participants = ["developer"]
|
|
13791
|
+
self.runtime_assigned_expert = "developer"
|
|
13792
|
+
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
13793
|
+
dq["active"] = False
|
|
13794
|
+
bb["decomposition_queue"] = dq
|
|
13795
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
13796
|
+
profile["execution_mode"] = EXECUTION_MODE_SINGLE
|
|
13797
|
+
profile["participants"] = ["developer"]
|
|
13798
|
+
profile["assigned_expert"] = "developer"
|
|
13799
|
+
bb["task_profile"] = profile
|
|
13800
|
+
self.blackboard = bb
|
|
13801
|
+
self._blackboard_touch()
|
|
13802
|
+
self._blackboard_history(
|
|
13803
|
+
"manager",
|
|
13804
|
+
trim(
|
|
13805
|
+
f"watchdog escalation: forced Single+Developer (trigger_count={int(bb.get('watchdog', {}).get('trigger_count', 0))}, reason={reason})",
|
|
13806
|
+
520,
|
|
13807
|
+
),
|
|
13808
|
+
)
|
|
13809
|
+
self._emit(
|
|
13810
|
+
"status",
|
|
13811
|
+
{
|
|
13812
|
+
"summary": (
|
|
13813
|
+
"watchdog escalation: multi-agent stall detected, "
|
|
13814
|
+
"downgrading to Single+Developer mode"
|
|
13815
|
+
)
|
|
13816
|
+
},
|
|
13817
|
+
)
|
|
13818
|
+
|
|
13356
13819
|
def _watchdog_pick_executor_route(self, board: dict | None = None) -> tuple[dict, dict] | None:
|
|
13357
13820
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13358
13821
|
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
@@ -13564,13 +14027,22 @@ class SessionState:
|
|
|
13564
14027
|
except Exception:
|
|
13565
14028
|
last_trigger_ts = 0.0
|
|
13566
14029
|
if now_ts() - last_trigger_ts >= 1.0:
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
)
|
|
14030
|
+
wd["trigger_count"] = max(0, int(wd.get("trigger_count", 0) or 0)) + 1
|
|
14031
|
+
wd["last_trigger_reason"] = trim(str(trigger_reason or "").strip(), 200)
|
|
14032
|
+
wd["last_trigger_ts"] = float(now_ts())
|
|
14033
|
+
bb["watchdog"] = wd
|
|
14034
|
+
self.blackboard = bb
|
|
14035
|
+
self._blackboard_touch()
|
|
14036
|
+
if int(wd["trigger_count"]) >= 2:
|
|
14037
|
+
self._watchdog_escalate_to_single_developer(bb, reason=trigger_reason)
|
|
14038
|
+
else:
|
|
14039
|
+
triggered = self._watchdog_activate_decomposition(
|
|
14040
|
+
bb,
|
|
14041
|
+
reason=trigger_reason,
|
|
14042
|
+
role=role,
|
|
14043
|
+
step=step,
|
|
14044
|
+
pinned_selection=pinned_selection,
|
|
14045
|
+
)
|
|
13574
14046
|
bb = self._ensure_blackboard()
|
|
13575
14047
|
bb["watchdog"] = self._normalize_watchdog_state(bb.get("watchdog", wd))
|
|
13576
14048
|
bb["decomposition_queue"] = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", dq))
|
|
@@ -13705,6 +14177,7 @@ class SessionState:
|
|
|
13705
14177
|
"text": "",
|
|
13706
14178
|
"ts": 0.0,
|
|
13707
14179
|
},
|
|
14180
|
+
"project_todos": [],
|
|
13708
14181
|
"watchdog": self._new_watchdog_state(),
|
|
13709
14182
|
"decomposition_queue": self._new_decomposition_queue_state(),
|
|
13710
14183
|
}
|
|
@@ -13856,6 +14329,24 @@ class SessionState:
|
|
|
13856
14329
|
"change_count": max(1, int(item.get("change_count", 1) or 1)),
|
|
13857
14330
|
}
|
|
13858
14331
|
board["code_artifacts"] = artifacts
|
|
14332
|
+
if not isinstance(bb_src_todos := src.get("project_todos"), list):
|
|
14333
|
+
board["project_todos"] = []
|
|
14334
|
+
else:
|
|
14335
|
+
clean_todos = []
|
|
14336
|
+
for pt in bb_src_todos[:20]:
|
|
14337
|
+
if not isinstance(pt, dict):
|
|
14338
|
+
continue
|
|
14339
|
+
clean_todos.append({
|
|
14340
|
+
"id": trim(str(pt.get("id", "") or ""), 20),
|
|
14341
|
+
"content": trim(str(pt.get("content", "") or ""), 400),
|
|
14342
|
+
"status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
|
|
14343
|
+
"category": trim(str(pt.get("category", "") or ""), 40),
|
|
14344
|
+
"created_at": float(pt.get("created_at", 0.0) or 0.0),
|
|
14345
|
+
"completed_at": float(pt.get("completed_at", 0.0) or 0.0) if pt.get("completed_at") else None,
|
|
14346
|
+
"completed_by": trim(str(pt.get("completed_by", "") or ""), 40),
|
|
14347
|
+
"evidence": trim(str(pt.get("evidence", "") or ""), 200),
|
|
14348
|
+
})
|
|
14349
|
+
board["project_todos"] = clean_todos
|
|
13859
14350
|
board["watchdog"] = self._normalize_watchdog_state(src.get("watchdog", {}))
|
|
13860
14351
|
board["decomposition_queue"] = self._normalize_decomposition_queue_state(
|
|
13861
14352
|
src.get("decomposition_queue", {})
|
|
@@ -13876,6 +14367,7 @@ class SessionState:
|
|
|
13876
14367
|
def _blackboard_reset_for_goal(self, goal: str):
|
|
13877
14368
|
self.blackboard = self._new_blackboard(goal)
|
|
13878
14369
|
self.manager_context = []
|
|
14370
|
+
self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != "manager"]
|
|
13879
14371
|
self.manager_routes = []
|
|
13880
14372
|
self._blackboard_history("manager", f"new goal accepted: {trim(goal, 300)}")
|
|
13881
14373
|
self._sync_todos_from_blackboard(reason="goal-reset", board=self.blackboard)
|
|
@@ -14186,13 +14678,150 @@ class SessionState:
|
|
|
14186
14678
|
row["activeForm"] = f"Pending ({label}): {text}"
|
|
14187
14679
|
return rows
|
|
14188
14680
|
|
|
14681
|
+
# ── Project-based todo generation & status tracking ──────────────
|
|
14682
|
+
|
|
14683
|
+
def _generate_project_todos_from_profile(self, board: dict | None = None) -> list[dict]:
|
|
14684
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14685
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
14686
|
+
task_type = str(profile.get("task_type", "general") or "general")
|
|
14687
|
+
objective = trim(str(profile.get("direct_objective", "") or ""), 200)
|
|
14688
|
+
|
|
14689
|
+
if task_type == "simple_qa":
|
|
14690
|
+
return [{"content": f"回答: {objective}" if objective else "回答用户问题", "category": "implement"}]
|
|
14691
|
+
|
|
14692
|
+
if task_type in ("simple_code", "engineering"):
|
|
14693
|
+
return [
|
|
14694
|
+
{"content": "分析需求和项目结构", "category": "setup"},
|
|
14695
|
+
{"content": f"实现: {objective}" if objective else "实现编码任务", "category": "implement"},
|
|
14696
|
+
{"content": "编译/语法检查", "category": "compile_test"},
|
|
14697
|
+
{"content": "最小功能测试", "category": "min_test"},
|
|
14698
|
+
]
|
|
14699
|
+
|
|
14700
|
+
if task_type == "research":
|
|
14701
|
+
return [
|
|
14702
|
+
{"content": f"调研: {objective}" if objective else "执行调研任务", "category": "implement"},
|
|
14703
|
+
{"content": "整理调研结果", "category": "review"},
|
|
14704
|
+
]
|
|
14705
|
+
|
|
14706
|
+
return [{"content": f"执行: {objective}" if objective else "执行任务", "category": "implement"}]
|
|
14707
|
+
|
|
14708
|
+
def _init_project_todos(self, board: dict | None = None):
|
|
14709
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14710
|
+
if bb.get("project_todos"):
|
|
14711
|
+
return
|
|
14712
|
+
raw = self._generate_project_todos_from_profile(bb)
|
|
14713
|
+
bb["project_todos"] = [
|
|
14714
|
+
{
|
|
14715
|
+
"id": f"pt:{i:03d}",
|
|
14716
|
+
"content": t["content"],
|
|
14717
|
+
"status": "pending",
|
|
14718
|
+
"category": t["category"],
|
|
14719
|
+
"created_at": float(now_ts()),
|
|
14720
|
+
"completed_at": None,
|
|
14721
|
+
"completed_by": "",
|
|
14722
|
+
"evidence": "",
|
|
14723
|
+
}
|
|
14724
|
+
for i, t in enumerate(raw)
|
|
14725
|
+
]
|
|
14726
|
+
self.blackboard = bb
|
|
14727
|
+
self._blackboard_touch()
|
|
14728
|
+
|
|
14729
|
+
def _has_compile_pass_evidence(self, board: dict | None = None) -> bool:
|
|
14730
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14731
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14732
|
+
if not logs:
|
|
14733
|
+
return False
|
|
14734
|
+
positive = ("compiled successfully", "build successful", "0 error", "编译成功",
|
|
14735
|
+
"syntax ok", "no errors", "build succeeded", "compilation successful")
|
|
14736
|
+
negative = ("error:", "fatal error", "syntax error", "compile error", "build failed")
|
|
14737
|
+
for entry in reversed(logs[-6:]):
|
|
14738
|
+
txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
|
|
14739
|
+
if not txt:
|
|
14740
|
+
continue
|
|
14741
|
+
if any(neg in txt for neg in negative):
|
|
14742
|
+
continue
|
|
14743
|
+
if any(pos in txt for pos in positive):
|
|
14744
|
+
return True
|
|
14745
|
+
return False
|
|
14746
|
+
|
|
14747
|
+
def _has_test_pass_evidence(self, board: dict | None = None) -> bool:
|
|
14748
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14749
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14750
|
+
feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
|
|
14751
|
+
positive = ("test passed", "tests passed", "测试通过", "运行正常",
|
|
14752
|
+
"all tests pass", "ok", "passed", "test succeeded")
|
|
14753
|
+
negative = ("failed", "error", "failure", "测试失败")
|
|
14754
|
+
combined = list(logs[-6:]) + list(feedback[-4:])
|
|
14755
|
+
for entry in reversed(combined[-8:]):
|
|
14756
|
+
txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
|
|
14757
|
+
if not txt:
|
|
14758
|
+
continue
|
|
14759
|
+
if any(neg in txt for neg in negative):
|
|
14760
|
+
continue
|
|
14761
|
+
if any(pos in txt for pos in positive):
|
|
14762
|
+
return True
|
|
14763
|
+
return False
|
|
14764
|
+
|
|
14765
|
+
def _update_project_todo_status(self, board: dict | None = None):
|
|
14766
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14767
|
+
todos = bb.get("project_todos", [])
|
|
14768
|
+
if not todos:
|
|
14769
|
+
return
|
|
14770
|
+
code_count = len(bb.get("code_artifacts", {}) or {})
|
|
14771
|
+
research_count = len(bb.get("research_notes", []) or [])
|
|
14772
|
+
feedback_pass = self._manager_feedback_passed_from_blackboard(bb)
|
|
14773
|
+
|
|
14774
|
+
for todo in todos:
|
|
14775
|
+
if todo.get("status") == "completed":
|
|
14776
|
+
continue
|
|
14777
|
+
cat = todo.get("category", "")
|
|
14778
|
+
if cat == "setup" and (research_count > 0 or code_count > 0):
|
|
14779
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="结构已分析")
|
|
14780
|
+
elif cat == "implement" and code_count > 0:
|
|
14781
|
+
todo.update(status="completed", completed_at=float(now_ts()),
|
|
14782
|
+
completed_by="developer", evidence=f"{code_count} 文件已产出")
|
|
14783
|
+
elif cat == "compile_test" and self._has_compile_pass_evidence(bb):
|
|
14784
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="编译通过")
|
|
14785
|
+
elif cat == "min_test" and self._has_test_pass_evidence(bb):
|
|
14786
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
|
|
14787
|
+
elif cat == "review" and feedback_pass:
|
|
14788
|
+
todo.update(status="completed", completed_at=float(now_ts()), evidence="审查通过")
|
|
14789
|
+
|
|
14790
|
+
if not any(t.get("status") == "in_progress" for t in todos):
|
|
14791
|
+
for t in todos:
|
|
14792
|
+
if t.get("status") == "pending":
|
|
14793
|
+
t["status"] = "in_progress"
|
|
14794
|
+
break
|
|
14795
|
+
|
|
14796
|
+
bb["project_todos"] = todos
|
|
14797
|
+
self.blackboard = bb
|
|
14798
|
+
|
|
14799
|
+
def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
|
|
14800
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14801
|
+
todos = bb.get("project_todos", [])
|
|
14802
|
+
if not todos:
|
|
14803
|
+
return self._todo_owner_rows_from_blackboard(bb)
|
|
14804
|
+
rows = []
|
|
14805
|
+
for todo in todos:
|
|
14806
|
+
s = todo.get("status", "pending")
|
|
14807
|
+
c = todo.get("content", "")
|
|
14808
|
+
ev = todo.get("evidence", "")
|
|
14809
|
+
af = {
|
|
14810
|
+
"in_progress": f"Working on: {c}",
|
|
14811
|
+
"completed": f"Done: {c}" + (f" ({ev})" if ev else ""),
|
|
14812
|
+
}.get(s, f"Pending: {c}")
|
|
14813
|
+
rows.append({"key": f"bb:proj:{todo.get('id', '')}", "content": c, "status": s, "activeForm": af})
|
|
14814
|
+
return rows
|
|
14815
|
+
|
|
14189
14816
|
def _sync_todos_from_blackboard(self, reason: str = "", board: dict | None = None):
|
|
14190
14817
|
if bool(self.runtime_reclassify_required):
|
|
14191
14818
|
return
|
|
14192
14819
|
if not self._is_multi_agent_mode():
|
|
14193
14820
|
return
|
|
14194
14821
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14195
|
-
|
|
14822
|
+
self._init_project_todos(bb)
|
|
14823
|
+
self._update_project_todo_status(bb)
|
|
14824
|
+
system_rows = self._todo_project_rows_from_blackboard(bb)
|
|
14196
14825
|
existing = self.todo.snapshot()
|
|
14197
14826
|
non_system_rows: list[dict] = []
|
|
14198
14827
|
for row in existing:
|
|
@@ -14200,18 +14829,17 @@ class SessionState:
|
|
|
14200
14829
|
continue
|
|
14201
14830
|
key = str(row.get("key", "") or "").strip()
|
|
14202
14831
|
owner = str(row.get("owner", "") or "").strip().lower()
|
|
14203
|
-
if key.startswith("bb:owner:"
|
|
14832
|
+
if key.startswith(("bb:owner:", "bb:node:", "bb:proj:")) or owner in {"manager", "explorer", "developer", "reviewer"}:
|
|
14204
14833
|
continue
|
|
14205
14834
|
non_system_rows.append(dict(row))
|
|
14206
14835
|
remaining_cap = max(0, 20 - len(system_rows))
|
|
14207
14836
|
merged = list(system_rows) + non_system_rows[:remaining_cap]
|
|
14208
14837
|
try:
|
|
14209
14838
|
todo_out = self.todo.update(merged)
|
|
14210
|
-
except Exception
|
|
14211
|
-
self._emit("status", {"summary": f"owner todo sync skipped: {trim(str(exc), 180)}"})
|
|
14839
|
+
except Exception:
|
|
14212
14840
|
return
|
|
14213
14841
|
if todo_out != "No todo changes." and reason:
|
|
14214
|
-
self._emit("status", {"summary": f"
|
|
14842
|
+
self._emit("status", {"summary": f"project todos synced ({trim(reason, 120)})"})
|
|
14215
14843
|
|
|
14216
14844
|
def _blackboard_set_status(self, status: str, note: str = ""):
|
|
14217
14845
|
board = self._ensure_blackboard()
|
|
@@ -14233,6 +14861,43 @@ class SessionState:
|
|
|
14233
14861
|
self._blackboard_touch()
|
|
14234
14862
|
self._sync_todos_from_blackboard(reason="approved", board=board)
|
|
14235
14863
|
|
|
14864
|
+
def _auto_summary_on_finish(self) -> str:
|
|
14865
|
+
"""Generate concise summary from blackboard state when run ends."""
|
|
14866
|
+
bb = self._ensure_blackboard()
|
|
14867
|
+
artifacts = bb.get("code_artifacts", {}) if isinstance(bb.get("code_artifacts"), dict) else {}
|
|
14868
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
14869
|
+
feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
|
|
14870
|
+
summary_parts = []
|
|
14871
|
+
if artifacts:
|
|
14872
|
+
file_list = ", ".join(list(artifacts.keys())[:10])
|
|
14873
|
+
summary_parts.append(f"Modified files: {file_list}")
|
|
14874
|
+
if feedback:
|
|
14875
|
+
last_fb = feedback[-1] if feedback else {}
|
|
14876
|
+
fb_content = str(last_fb.get("content", "") or "") if isinstance(last_fb, dict) else str(last_fb)
|
|
14877
|
+
if fb_content:
|
|
14878
|
+
summary_parts.append(f"Review: {trim(fb_content, 200)}")
|
|
14879
|
+
if logs:
|
|
14880
|
+
recent_logs = logs[-3:]
|
|
14881
|
+
log_strs = []
|
|
14882
|
+
for log_entry in recent_logs:
|
|
14883
|
+
if isinstance(log_entry, dict):
|
|
14884
|
+
log_strs.append(trim(str(log_entry.get("content", "") or ""), 80))
|
|
14885
|
+
elif isinstance(log_entry, str):
|
|
14886
|
+
log_strs.append(trim(log_entry, 80))
|
|
14887
|
+
if log_strs:
|
|
14888
|
+
summary_parts.append(f"Logs: {'; '.join(log_strs)}")
|
|
14889
|
+
summary = "; ".join(summary_parts) or "Task completed."
|
|
14890
|
+
bb["status"] = "COMPLETED"
|
|
14891
|
+
bb["approval"] = {
|
|
14892
|
+
"approved": True,
|
|
14893
|
+
"by": "auto",
|
|
14894
|
+
"note": summary,
|
|
14895
|
+
"ts": float(now_ts()),
|
|
14896
|
+
}
|
|
14897
|
+
self._blackboard_touch()
|
|
14898
|
+
self._emit("status", {"summary": f"[auto-summary] {trim(summary, 400)}"})
|
|
14899
|
+
return summary
|
|
14900
|
+
|
|
14236
14901
|
def _blackboard_read_state_markdown(self, max_items: int = 6) -> str:
|
|
14237
14902
|
board = self._ensure_blackboard()
|
|
14238
14903
|
profile = self._ensure_blackboard_task_profile(board)
|
|
@@ -14347,6 +15012,23 @@ class SessionState:
|
|
|
14347
15012
|
lines.append("- (none)")
|
|
14348
15013
|
_render_tail("Recent Execution Logs", board.get("execution_logs", []))
|
|
14349
15014
|
_render_tail("Recent Review Feedback", board.get("review_feedback", []))
|
|
15015
|
+
|
|
15016
|
+
proj_todos = board.get("project_todos", [])
|
|
15017
|
+
if proj_todos:
|
|
15018
|
+
lines.append("\n### Project Tasks")
|
|
15019
|
+
for pt in proj_todos:
|
|
15020
|
+
s = pt.get("status", "pending")
|
|
15021
|
+
c = trim(str(pt.get("content", "") or ""), 200)
|
|
15022
|
+
ev = trim(str(pt.get("evidence", "") or ""), 100)
|
|
15023
|
+
if s == "completed":
|
|
15024
|
+
mark = "[x]"
|
|
15025
|
+
elif s == "in_progress":
|
|
15026
|
+
mark = "[>]"
|
|
15027
|
+
else:
|
|
15028
|
+
mark = "[ ]"
|
|
15029
|
+
suffix = f" — {ev}" if ev else ""
|
|
15030
|
+
lines.append(f"- {mark} {c}{suffix}")
|
|
15031
|
+
|
|
14350
15032
|
return "\n".join(lines)
|
|
14351
15033
|
|
|
14352
15034
|
def _manager_route_tools(self) -> list[dict]:
|
|
@@ -14612,7 +15294,7 @@ class SessionState:
|
|
|
14612
15294
|
"judgement": trim(str(profile.get("reason", "") or "manager fallback classification"), 200),
|
|
14613
15295
|
"round_budget": int(policy.get("round_budget", profile.get("round_budget", self.max_agent_rounds)) or 0),
|
|
14614
15296
|
"direct_objective": trim(str(profile.get("direct_objective", "") or ""), 800),
|
|
14615
|
-
"execution_mode": str(policy.get("execution_mode", EXECUTION_MODE_SYNC)),
|
|
15297
|
+
"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
15298
|
"participants": participants,
|
|
14617
15299
|
"assigned_expert": assigned,
|
|
14618
15300
|
"requires_user_confirmation": bool(requires_confirmation),
|
|
@@ -14622,7 +15304,7 @@ class SessionState:
|
|
|
14622
15304
|
}
|
|
14623
15305
|
|
|
14624
15306
|
def _manager_classification_system_prompt(self) -> str:
|
|
14625
|
-
|
|
15307
|
+
base = (
|
|
14626
15308
|
"You are Manager. Classify the latest user request by semantic intent, not by keyword templates. "
|
|
14627
15309
|
"Decide whether this latest turn should inherit the previous blackboard/task state. "
|
|
14628
15310
|
"Set inherit_previous_state=true only for genuine follow-up/continuation/refinement of the same ongoing work; "
|
|
@@ -14645,6 +15327,12 @@ class SessionState:
|
|
|
14645
15327
|
"Use low confidence only when semantic ambiguity is substantial, then set low_confidence_reason briefly. "
|
|
14646
15328
|
f"{model_language_instruction(self.ui_language)}"
|
|
14647
15329
|
)
|
|
15330
|
+
if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
|
|
15331
|
+
base += (
|
|
15332
|
+
" NOTE: User has configured Single-agent mode. "
|
|
15333
|
+
"Favor level 1-2 for straightforward tasks; only assign level 3+ when genuine multi-step complexity demands it."
|
|
15334
|
+
)
|
|
15335
|
+
return base
|
|
14648
15336
|
|
|
14649
15337
|
def _apply_runtime_task_decision(self, goal_text: str, decision: dict):
|
|
14650
15338
|
row = dict(decision or {})
|
|
@@ -14666,7 +15354,12 @@ class SessionState:
|
|
|
14666
15354
|
if level not in TASK_LEVEL_CHOICES:
|
|
14667
15355
|
level = 3
|
|
14668
15356
|
policy = dict(TASK_LEVEL_POLICIES.get(level, TASK_LEVEL_POLICIES[3]))
|
|
14669
|
-
|
|
15357
|
+
policy_mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
|
|
15358
|
+
config_mode = normalize_execution_mode(self.execution_mode, default="")
|
|
15359
|
+
if config_mode == EXECUTION_MODE_SINGLE:
|
|
15360
|
+
mode = EXECUTION_MODE_SINGLE
|
|
15361
|
+
else:
|
|
15362
|
+
mode = policy_mode
|
|
14670
15363
|
assigned = self._sanitize_agent_role(
|
|
14671
15364
|
row.get("assigned_expert", policy.get("assigned_expert", "developer"))
|
|
14672
15365
|
) or self._sanitize_agent_role(policy.get("assigned_expert", "developer")) or "developer"
|
|
@@ -14697,6 +15390,9 @@ class SessionState:
|
|
|
14697
15390
|
except Exception:
|
|
14698
15391
|
budget_raw = int(policy.get("round_budget", self.max_agent_rounds) or self.max_agent_rounds)
|
|
14699
15392
|
round_budget = max(1, min(int(self.max_agent_rounds or MAX_AGENT_ROUNDS), int(budget_raw)))
|
|
15393
|
+
if mode == EXECUTION_MODE_SINGLE and policy_mode != EXECUTION_MODE_SINGLE:
|
|
15394
|
+
policy_budget = int(policy.get("round_budget", 10) or 10)
|
|
15395
|
+
round_budget = min(round_budget, max(4, policy_budget // 2))
|
|
14700
15396
|
requires_confirmation = bool(row.get("requires_user_confirmation", policy.get("requires_user_confirmation", False)))
|
|
14701
15397
|
if level == 5 and self._looks_like_positive_confirmation(goal_text):
|
|
14702
15398
|
requires_confirmation = False
|
|
@@ -14875,8 +15571,14 @@ class SessionState:
|
|
|
14875
15571
|
"When user preference is clear, prioritize it over your default plan. "
|
|
14876
15572
|
"Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
|
|
14877
15573
|
"Also decide inherit_previous_state by semantic continuity with prior blackboard state. "
|
|
14878
|
-
"If this is pure continuation, keep current runtime policy unchanged
|
|
15574
|
+
"If this is pure continuation, keep current runtime policy unchanged.\n"
|
|
15575
|
+
f"User configured execution mode: {self.execution_mode}\n"
|
|
14879
15576
|
)
|
|
15577
|
+
if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
|
|
15578
|
+
prompt += (
|
|
15579
|
+
"IMPORTANT: User has configured Single-agent mode. "
|
|
15580
|
+
"Prefer level 1-2 for simple tasks. Only escalate to level 3+ if truly complex.\n"
|
|
15581
|
+
)
|
|
14880
15582
|
with self.lock:
|
|
14881
15583
|
self.current_phase = "manager:classify:model-call"
|
|
14882
15584
|
self.current_tool_name = ""
|
|
@@ -14959,6 +15661,22 @@ class SessionState:
|
|
|
14959
15661
|
self._apply_runtime_task_decision(goal, decision)
|
|
14960
15662
|
return dict(decision or {})
|
|
14961
15663
|
|
|
15664
|
+
def _project_todo_hint_for_manager(self) -> str:
|
|
15665
|
+
bb = self._ensure_blackboard()
|
|
15666
|
+
todos = bb.get("project_todos", [])
|
|
15667
|
+
if not todos:
|
|
15668
|
+
return ""
|
|
15669
|
+
pending = [t for t in todos if t.get("status") != "completed"]
|
|
15670
|
+
if not pending:
|
|
15671
|
+
return "All project tasks completed. Route to finish. "
|
|
15672
|
+
cur = pending[0]
|
|
15673
|
+
cat = cur.get("category", "")
|
|
15674
|
+
if cat == "compile_test":
|
|
15675
|
+
return "NEXT: compile/syntax check required. Route to Developer for build. "
|
|
15676
|
+
if cat == "min_test":
|
|
15677
|
+
return "NEXT: minimal test required. Route to Developer to run tests. "
|
|
15678
|
+
return f"NEXT: {trim(str(cur.get('content', '') or ''), 120)}. "
|
|
15679
|
+
|
|
14962
15680
|
def _manager_system_prompt(self) -> str:
|
|
14963
15681
|
board = self._ensure_blackboard()
|
|
14964
15682
|
profile = self._ensure_blackboard_task_profile(board)
|
|
@@ -14966,41 +15684,30 @@ class SessionState:
|
|
|
14966
15684
|
budget = self._blackboard_round_budget(board)
|
|
14967
15685
|
level = int(profile.get("task_level", self.runtime_task_level or 0) or 0)
|
|
14968
15686
|
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
|
-
|
|
15687
|
+
task_type = str(profile.get("task_type", "general") or "general").strip().lower()
|
|
15688
|
+
coding_hint = ""
|
|
15689
|
+
if task_type in ("simple_code", "engineering"):
|
|
15690
|
+
coding_hint = (
|
|
15691
|
+
"CODING TASK: skip lengthy exploration/design phases. "
|
|
15692
|
+
"Route to Developer early for implementation. "
|
|
15693
|
+
"Explorer should only be used for specific file/API lookups, not broad analysis. "
|
|
15694
|
+
)
|
|
15695
|
+
project_todo_hint = self._project_todo_hint_for_manager()
|
|
14978
15696
|
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)}, "
|
|
15697
|
+
"You are Manager in a multi-agent coding system. "
|
|
15698
|
+
"Read blackboard, delegate one short timeslice via route_to_next_agent. "
|
|
15699
|
+
"Policy: missing facts->explorer, implementation->developer, verification->reviewer, "
|
|
15700
|
+
"all done->finish. Set is_mandatory=true when concrete execution is required. "
|
|
15701
|
+
"Role capabilities: "
|
|
15702
|
+
"Explorer=read-only (bash/read_file/search/blackboard, NO write_file/edit_file); "
|
|
15703
|
+
"Developer=all tools (write_file/edit_file/bash/read_file/etc); "
|
|
15704
|
+
"Reviewer=read+verify (bash/read_file/finish_task, NO write_file/edit_file). "
|
|
15705
|
+
"NEVER delegate file-writing tasks to Explorer or Reviewer. "
|
|
15706
|
+
f"{coding_hint}"
|
|
15707
|
+
f"{project_todo_hint}"
|
|
15708
|
+
f"Level={level}, mode={mode}, progress={progress}, "
|
|
15709
|
+
f"budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
|
|
15002
15710
|
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
15711
|
f"{model_language_instruction(self.ui_language)}"
|
|
15005
15712
|
)
|
|
15006
15713
|
|
|
@@ -15243,6 +15950,24 @@ class SessionState:
|
|
|
15243
15950
|
"reason": "approval-blocked-by-error",
|
|
15244
15951
|
"source": "fallback",
|
|
15245
15952
|
}
|
|
15953
|
+
if finish_gate_reason.startswith("project-todo-incomplete:"):
|
|
15954
|
+
missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
|
|
15955
|
+
if missing_cat == "compile_test":
|
|
15956
|
+
return {
|
|
15957
|
+
"target": "developer",
|
|
15958
|
+
"instruction": "编译/语法检查尚未完成。请编译项目并确认无错误。",
|
|
15959
|
+
"reason": "project-todo-compile-pending",
|
|
15960
|
+
"source": "fallback",
|
|
15961
|
+
"is_mandatory": True,
|
|
15962
|
+
}
|
|
15963
|
+
if missing_cat == "min_test":
|
|
15964
|
+
return {
|
|
15965
|
+
"target": "developer",
|
|
15966
|
+
"instruction": "最小功能测试尚未完成。请运行基本测试验证核心功能。",
|
|
15967
|
+
"reason": "project-todo-test-pending",
|
|
15968
|
+
"source": "fallback",
|
|
15969
|
+
"is_mandatory": True,
|
|
15970
|
+
}
|
|
15246
15971
|
if task_type == "simple_qa":
|
|
15247
15972
|
dev_text = self._latest_agent_assistant_text("developer")
|
|
15248
15973
|
if dev_text:
|
|
@@ -15263,6 +15988,29 @@ class SessionState:
|
|
|
15263
15988
|
"reason": "simple-qa-direct-answer",
|
|
15264
15989
|
"source": "fallback",
|
|
15265
15990
|
}
|
|
15991
|
+
# ── 通用 endpoint 检测:非 simple_qa 的 developer 结论性回复也能触发 finish ──
|
|
15992
|
+
if task_type != "simple_qa":
|
|
15993
|
+
dev_text = self._latest_agent_assistant_text("developer")
|
|
15994
|
+
if dev_text:
|
|
15995
|
+
done_probe = self._detect_endpoint_intent(dev_text, None)
|
|
15996
|
+
if bool(done_probe.get("matched", False)) and not has_error_log:
|
|
15997
|
+
return {
|
|
15998
|
+
"target": "finish",
|
|
15999
|
+
"instruction": "Developer has provided a conclusive response; stop now.",
|
|
16000
|
+
"reason": "general-endpoint-detected",
|
|
16001
|
+
"source": "fallback",
|
|
16002
|
+
}
|
|
16003
|
+
# 通用检查:如果最近的 assistant 消息是结论性回复,且没有待办事项,直接 finish
|
|
16004
|
+
if not has_error_log:
|
|
16005
|
+
for _role in ("developer", "explorer", "reviewer"):
|
|
16006
|
+
_last = self._latest_agent_assistant_text(_role)
|
|
16007
|
+
if _last and self._looks_like_conclusive_reply(_last) and not self.todo.has_open_items():
|
|
16008
|
+
return {
|
|
16009
|
+
"target": "finish",
|
|
16010
|
+
"instruction": "Agent already provided a conclusive reply with no open tasks; stop now.",
|
|
16011
|
+
"reason": "conclusive-reply-detected",
|
|
16012
|
+
"source": "fallback",
|
|
16013
|
+
}
|
|
15266
16014
|
if complexity == "simple" and task_type == "simple_code":
|
|
15267
16015
|
if has_error_log:
|
|
15268
16016
|
return {
|
|
@@ -15398,6 +16146,24 @@ class SessionState:
|
|
|
15398
16146
|
if str(row.get("task_type", "") or "").strip().lower() == "simple_qa":
|
|
15399
16147
|
return row
|
|
15400
16148
|
target = str(row.get("target", "") or "").strip().lower()
|
|
16149
|
+
task_type_low = str(row.get("task_type", "") or "").strip().lower()
|
|
16150
|
+
if task_type_low in ("simple_code", "engineering") and target == "explorer":
|
|
16151
|
+
board = self._ensure_blackboard()
|
|
16152
|
+
progress = self._manager_progress_state(board)
|
|
16153
|
+
if progress in ("initializing", "in_progress"):
|
|
16154
|
+
explorer_count = sum(
|
|
16155
|
+
1 for x in self.manager_routes[-8:]
|
|
16156
|
+
if str(x.get("target", "") or "").strip().lower() == "explorer"
|
|
16157
|
+
)
|
|
16158
|
+
if explorer_count >= 2:
|
|
16159
|
+
row["target"] = "developer"
|
|
16160
|
+
row["instruction"] = (
|
|
16161
|
+
"Coding task: Explorer has been used enough. "
|
|
16162
|
+
"Start implementation now using write_file/edit_file."
|
|
16163
|
+
)
|
|
16164
|
+
row["reason"] = f"{row.get('reason', '')}|coding-fast-track->developer"
|
|
16165
|
+
row["source"] = "anti-stall"
|
|
16166
|
+
return row
|
|
15401
16167
|
if target not in AGENT_ROLES:
|
|
15402
16168
|
return row
|
|
15403
16169
|
recent = [str(x.get("target", "") or "").strip().lower() for x in self.manager_routes[-4:]]
|
|
@@ -15492,6 +16258,11 @@ class SessionState:
|
|
|
15492
16258
|
participants[-1] = target
|
|
15493
16259
|
else:
|
|
15494
16260
|
target = participants[0]
|
|
16261
|
+
# ── Single 模式硬约束:无论 executor_mode_flag 如何,只允许 assigned_expert ──
|
|
16262
|
+
if mode == EXECUTION_MODE_SINGLE:
|
|
16263
|
+
participants = [assigned_expert]
|
|
16264
|
+
if target in AGENT_ROLES and target != assigned_expert:
|
|
16265
|
+
target = assigned_expert
|
|
15495
16266
|
instruction = trim(str(row.get("instruction", "") or "").strip(), 1200)
|
|
15496
16267
|
if not instruction:
|
|
15497
16268
|
instruction = "Proceed with one concrete next step and report evidence."
|
|
@@ -15548,6 +16319,24 @@ class SessionState:
|
|
|
15548
16319
|
feedback_pass = self._manager_feedback_passed_from_blackboard(board)
|
|
15549
16320
|
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
15550
16321
|
force_finish_override = False
|
|
16322
|
+
# ── 结论性回复截断:当 Agent 已回复结论且无待办/无错误时,强制 finish ──
|
|
16323
|
+
if target in AGENT_ROLES and target != "finish":
|
|
16324
|
+
for _check_role in ("developer", "explorer", "reviewer"):
|
|
16325
|
+
_last_text = self._latest_agent_assistant_text(_check_role)
|
|
16326
|
+
if (
|
|
16327
|
+
_last_text
|
|
16328
|
+
and self._looks_like_conclusive_reply(_last_text)
|
|
16329
|
+
and not self.todo.has_open_items()
|
|
16330
|
+
and not self._manager_has_error_log(board)
|
|
16331
|
+
):
|
|
16332
|
+
target = "finish"
|
|
16333
|
+
instruction = (
|
|
16334
|
+
f"Agent '{_check_role}' already provided a conclusive reply. "
|
|
16335
|
+
"No open tasks remain. Finishing now."
|
|
16336
|
+
)
|
|
16337
|
+
row["reason"] = f"conclusive-reply-override:{_check_role}"
|
|
16338
|
+
row["source"] = "policy"
|
|
16339
|
+
break
|
|
15551
16340
|
if bool((board.get("approval", {}) or {}).get("approved", False)) and can_finish_from_approval:
|
|
15552
16341
|
target = "finish"
|
|
15553
16342
|
if not instruction:
|
|
@@ -15616,6 +16405,44 @@ class SessionState:
|
|
|
15616
16405
|
"Do not finish yet. Latest execution logs still contain blocking errors. "
|
|
15617
16406
|
"Resolve errors and provide verifiable evidence."
|
|
15618
16407
|
)
|
|
16408
|
+
elif finish_gate_reason.startswith("project-todo-incomplete:"):
|
|
16409
|
+
missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
|
|
16410
|
+
target = "developer"
|
|
16411
|
+
if missing_cat == "compile_test":
|
|
16412
|
+
instruction = (
|
|
16413
|
+
"编译/语法检查尚未完成。请编译项目并确认无错误。"
|
|
16414
|
+
"Run build/compile and confirm zero errors before finishing."
|
|
16415
|
+
)
|
|
16416
|
+
elif missing_cat == "min_test":
|
|
16417
|
+
instruction = (
|
|
16418
|
+
"最小功能测试尚未完成。请运行基本测试验证核心功能。"
|
|
16419
|
+
"Run minimal tests to verify core functionality before finishing."
|
|
16420
|
+
)
|
|
16421
|
+
else:
|
|
16422
|
+
instruction = (
|
|
16423
|
+
"Project todo not yet completed. Execute the pending step and report evidence."
|
|
16424
|
+
)
|
|
16425
|
+
# Force-finish fallback: if blocked > 3 cycles, mark as done to avoid deadloop
|
|
16426
|
+
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
16427
|
+
if summary_attempts >= 3:
|
|
16428
|
+
force_finish_override = True
|
|
16429
|
+
target = "finish"
|
|
16430
|
+
for pt in board.get("project_todos", []):
|
|
16431
|
+
if pt.get("category") in ("compile_test", "min_test") and pt.get("status") != "completed":
|
|
16432
|
+
pt.update(status="completed", completed_at=float(now_ts()),
|
|
16433
|
+
evidence="force-finish fallback")
|
|
16434
|
+
self.blackboard = board
|
|
16435
|
+
instruction = (
|
|
16436
|
+
"Compile/test gate exceeded retry limit. Force finishing with available evidence. "
|
|
16437
|
+
"Generate final summary and finish now."
|
|
16438
|
+
)
|
|
16439
|
+
row["reason"] = "finish-blocked-project-todo-force-close"
|
|
16440
|
+
row["source"] = "policy"
|
|
16441
|
+
else:
|
|
16442
|
+
board["manager_summary_attempts"] = summary_attempts + 1
|
|
16443
|
+
self.blackboard = board
|
|
16444
|
+
row["reason"] = f"finish-blocked-{missing_cat}"
|
|
16445
|
+
row["source"] = "policy"
|
|
15619
16446
|
else:
|
|
15620
16447
|
has_outputs = bool(code_count > 0 or research_count > 0)
|
|
15621
16448
|
if board_status == "COMPLETED" and has_outputs:
|
|
@@ -15848,7 +16675,7 @@ class SessionState:
|
|
|
15848
16675
|
},
|
|
15849
16676
|
}
|
|
15850
16677
|
]
|
|
15851
|
-
self.
|
|
16678
|
+
self._append_manager_context(
|
|
15852
16679
|
{
|
|
15853
16680
|
"role": "system",
|
|
15854
16681
|
"content": (
|
|
@@ -15858,7 +16685,6 @@ class SessionState:
|
|
|
15858
16685
|
"ts": now_ts(),
|
|
15859
16686
|
}
|
|
15860
16687
|
)
|
|
15861
|
-
self.manager_context = self.manager_context[-400:]
|
|
15862
16688
|
self._emit(
|
|
15863
16689
|
"status",
|
|
15864
16690
|
{
|
|
@@ -15898,7 +16724,7 @@ class SessionState:
|
|
|
15898
16724
|
},
|
|
15899
16725
|
}
|
|
15900
16726
|
]
|
|
15901
|
-
self.
|
|
16727
|
+
self._append_manager_context(
|
|
15902
16728
|
{
|
|
15903
16729
|
"role": "system",
|
|
15904
16730
|
"content": (
|
|
@@ -15908,7 +16734,6 @@ class SessionState:
|
|
|
15908
16734
|
"ts": now_ts(),
|
|
15909
16735
|
}
|
|
15910
16736
|
)
|
|
15911
|
-
self.manager_context = self.manager_context[-400:]
|
|
15912
16737
|
self._emit(
|
|
15913
16738
|
"status",
|
|
15914
16739
|
{
|
|
@@ -15926,8 +16751,8 @@ class SessionState:
|
|
|
15926
16751
|
"Return only one route_to_next_agent call.\n\n"
|
|
15927
16752
|
f"{self._blackboard_read_state_markdown(max_items=6)}"
|
|
15928
16753
|
)
|
|
15929
|
-
self.
|
|
15930
|
-
self.
|
|
16754
|
+
self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
|
|
16755
|
+
self._microcompact_agent_messages(self.manager_context)
|
|
15931
16756
|
with self.lock:
|
|
15932
16757
|
self.current_phase = "manager:model-call"
|
|
15933
16758
|
self.current_tool_name = ""
|
|
@@ -15963,8 +16788,7 @@ class SessionState:
|
|
|
15963
16788
|
}
|
|
15964
16789
|
for tc in tool_calls
|
|
15965
16790
|
]
|
|
15966
|
-
self.
|
|
15967
|
-
self.manager_context = self.manager_context[-400:]
|
|
16791
|
+
self._append_manager_context(assistant)
|
|
15968
16792
|
route_only_tool_calls = False
|
|
15969
16793
|
if isinstance(tool_calls, list) and tool_calls:
|
|
15970
16794
|
tool_names = [
|
|
@@ -16241,6 +17065,9 @@ class SessionState:
|
|
|
16241
17065
|
"agent_role": role_key,
|
|
16242
17066
|
}
|
|
16243
17067
|
self.contexts[role_key] = [executor_seed]
|
|
17068
|
+
# Also filter old messages for this role from agent_messages and add the seed
|
|
17069
|
+
self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != role_key]
|
|
17070
|
+
self.agent_messages.append(executor_seed)
|
|
16244
17071
|
self._emit(
|
|
16245
17072
|
"status",
|
|
16246
17073
|
{
|
|
@@ -16259,11 +17086,22 @@ class SessionState:
|
|
|
16259
17086
|
max_len=1400,
|
|
16260
17087
|
)
|
|
16261
17088
|
language_note = embedded_policy or self._agent_language_policy_note()
|
|
17089
|
+
role_capability_note = {
|
|
17090
|
+
"explorer": "YOUR TOOLS: read-only (bash/read_file/search/blackboard). You CANNOT write_file or edit_file.",
|
|
17091
|
+
"developer": "YOUR TOOLS: all tools available (write_file/edit_file/bash/read_file/etc).",
|
|
17092
|
+
"reviewer": "YOUR TOOLS: read+verify (bash/read_file/finish_task). You CANNOT write_file or edit_file.",
|
|
17093
|
+
}.get(role_key, "")
|
|
17094
|
+
if role_key == "explorer":
|
|
17095
|
+
tool_examples = "bash/read_file/read_from_blackboard"
|
|
17096
|
+
elif role_key == "reviewer":
|
|
17097
|
+
tool_examples = "bash/read_file/finish_task"
|
|
17098
|
+
else:
|
|
17099
|
+
tool_examples = "write_file/edit_file/bash/read_file"
|
|
16262
17100
|
mandatory_note = (
|
|
16263
17101
|
(
|
|
16264
17102
|
"MANDATORY EXECUTION: this delegate is hard-push. "
|
|
16265
17103
|
"You must call at least one concrete tool in this turn "
|
|
16266
|
-
"(e.g.
|
|
17104
|
+
f"(e.g. {tool_examples}) and produce verifiable progress. "
|
|
16267
17105
|
"Suggestion-only text reply is forbidden."
|
|
16268
17106
|
)
|
|
16269
17107
|
if bool(is_mandatory)
|
|
@@ -16293,6 +17131,7 @@ class SessionState:
|
|
|
16293
17131
|
f"{mandatory_note}\n"
|
|
16294
17132
|
f"{executor_note}\n"
|
|
16295
17133
|
f"{collaboration_note}\n"
|
|
17134
|
+
f"{role_capability_note}\n"
|
|
16296
17135
|
"</manager-delegate>\n"
|
|
16297
17136
|
"<blackboard-state>\n"
|
|
16298
17137
|
f"{trim(board_md, 6000)}\n"
|
|
@@ -16802,10 +17641,17 @@ class SessionState:
|
|
|
16802
17641
|
role_key = self._sanitize_agent_role(role)
|
|
16803
17642
|
if not role_key:
|
|
16804
17643
|
return self.messages
|
|
17644
|
+
# Build filtered view from unified agent_messages
|
|
17645
|
+
filtered = [
|
|
17646
|
+
m for m in self.agent_messages
|
|
17647
|
+
if m.get("agent_role") == role_key
|
|
17648
|
+
or m.get("agent_role") == "shared"
|
|
17649
|
+
or (m.get("role") == "user" and not m.get("agent_role"))
|
|
17650
|
+
]
|
|
17651
|
+
# Update legacy cache for backward compatibility (serialization, etc.)
|
|
16805
17652
|
if not isinstance(self.contexts, dict):
|
|
16806
17653
|
self.contexts = {r: [] for r in AGENT_ROLES}
|
|
16807
|
-
|
|
16808
|
-
self.contexts[role_key] = []
|
|
17654
|
+
self.contexts[role_key] = filtered[-400:]
|
|
16809
17655
|
return self.contexts[role_key]
|
|
16810
17656
|
|
|
16811
17657
|
def _append_agent_context_message(self, role: str, message: dict, *, mirror_to_global: bool = False) -> dict:
|
|
@@ -16814,11 +17660,10 @@ class SessionState:
|
|
|
16814
17660
|
row["agent_role"] = role_key
|
|
16815
17661
|
if "ts" not in row:
|
|
16816
17662
|
row["ts"] = now_ts()
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
if len(
|
|
16820
|
-
self.
|
|
16821
|
-
ctx = self.contexts[role_key]
|
|
17663
|
+
# Write to unified agent_messages
|
|
17664
|
+
self.agent_messages.append(row)
|
|
17665
|
+
if len(self.agent_messages) > 1200:
|
|
17666
|
+
self.agent_messages = self.agent_messages[-800:]
|
|
16822
17667
|
if mirror_to_global:
|
|
16823
17668
|
mirror = dict(row)
|
|
16824
17669
|
if "tool_calls" in mirror and isinstance(mirror.get("tool_calls"), list):
|
|
@@ -16838,6 +17683,19 @@ class SessionState:
|
|
|
16838
17683
|
self.messages = self.messages[-400:]
|
|
16839
17684
|
return row
|
|
16840
17685
|
|
|
17686
|
+
def _append_manager_context(self, message: dict):
|
|
17687
|
+
"""Append to manager_context and agent_messages in sync."""
|
|
17688
|
+
row = dict(message or {})
|
|
17689
|
+
if "agent_role" not in row:
|
|
17690
|
+
row["agent_role"] = "manager"
|
|
17691
|
+
if "ts" not in row:
|
|
17692
|
+
row["ts"] = now_ts()
|
|
17693
|
+
self.manager_context.append(row)
|
|
17694
|
+
self.manager_context = self.manager_context[-400:]
|
|
17695
|
+
self.agent_messages.append(row)
|
|
17696
|
+
if len(self.agent_messages) > 1200:
|
|
17697
|
+
self.agent_messages = self.agent_messages[-800:]
|
|
17698
|
+
|
|
16841
17699
|
def _agent_display_name(self, role: str) -> str:
|
|
16842
17700
|
return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
|
|
16843
17701
|
|
|
@@ -16935,52 +17793,69 @@ class SessionState:
|
|
|
16935
17793
|
)
|
|
16936
17794
|
return envelope
|
|
16937
17795
|
|
|
17796
|
+
def _drain_agentbus_fast_route(self) -> dict | None:
|
|
17797
|
+
"""Check agent_bus_messages for an unprocessed handoff that can skip manager.
|
|
17798
|
+
|
|
17799
|
+
Returns route dict with 'to' and 'payload' if a valid fast-route is found,
|
|
17800
|
+
otherwise returns None (fall back to manager delegation).
|
|
17801
|
+
"""
|
|
17802
|
+
if not self.agent_bus_messages:
|
|
17803
|
+
return None
|
|
17804
|
+
now = now_ts()
|
|
17805
|
+
valid_intents = {
|
|
17806
|
+
"handoff", "review_request", "fix_request",
|
|
17807
|
+
"final_summary_request", "implementation_ready",
|
|
17808
|
+
}
|
|
17809
|
+
for env in reversed(self.agent_bus_messages[-20:]):
|
|
17810
|
+
if not isinstance(env, dict):
|
|
17811
|
+
continue
|
|
17812
|
+
if env.get("_fast_routed"):
|
|
17813
|
+
continue
|
|
17814
|
+
intent = str(env.get("intent", "") or "").strip().lower()
|
|
17815
|
+
if intent not in valid_intents:
|
|
17816
|
+
continue
|
|
17817
|
+
to_role = self._sanitize_agent_role(env.get("to", ""))
|
|
17818
|
+
if not to_role or to_role not in AGENT_ROLES:
|
|
17819
|
+
continue
|
|
17820
|
+
age = max(0.0, now - float(env.get("ts", 0.0) or 0.0))
|
|
17821
|
+
if age > 180.0:
|
|
17822
|
+
continue
|
|
17823
|
+
env["_fast_routed"] = True
|
|
17824
|
+
payload = trim(str(env.get("payload", "") or ""), 1400)
|
|
17825
|
+
from_role = str(env.get("from", "") or "")
|
|
17826
|
+
self._emit(
|
|
17827
|
+
"status",
|
|
17828
|
+
{
|
|
17829
|
+
"summary": (
|
|
17830
|
+
f"agentbus fast-route: {from_role}->{to_role} "
|
|
17831
|
+
f"intent={intent} (skipping manager)"
|
|
17832
|
+
)
|
|
17833
|
+
},
|
|
17834
|
+
)
|
|
17835
|
+
return {
|
|
17836
|
+
"to": to_role,
|
|
17837
|
+
"payload": payload,
|
|
17838
|
+
"intent": intent,
|
|
17839
|
+
"from": from_role,
|
|
17840
|
+
"env_id": env.get("id", ""),
|
|
17841
|
+
}
|
|
17842
|
+
return None
|
|
17843
|
+
|
|
16938
17844
|
def _agent_role_system_prompt(self, role: str) -> str:
|
|
16939
17845
|
role_key = self._sanitize_agent_role(role) or "developer"
|
|
16940
17846
|
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. "
|
|
17847
|
+
f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
|
|
17848
|
+
f"Workspace: {self.files_root}. Use relative paths. "
|
|
17849
|
+
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
17850
|
+
"Keep outputs concise and action-oriented. "
|
|
17851
|
+
"Use load_skill for detailed guidance (multi-agent-guide, code-review-checklist, finish-protocol). "
|
|
16953
17852
|
f"{model_language_instruction(self.ui_language)} "
|
|
16954
17853
|
)
|
|
16955
17854
|
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
|
-
)
|
|
17855
|
+
return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
|
|
16964
17856
|
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
|
-
)
|
|
17857
|
+
return base + "Role: verify output, run checks, issue pass/fix decisions. Write review_feedback to blackboard. "
|
|
17858
|
+
return base + "Role: implement code changes, execute tools, record progress to blackboard. "
|
|
16984
17859
|
|
|
16985
17860
|
def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
|
|
16986
17861
|
if not self._is_multi_agent_mode():
|
|
@@ -17005,16 +17880,17 @@ class SessionState:
|
|
|
17005
17880
|
mirror_to_global=False,
|
|
17006
17881
|
)
|
|
17007
17882
|
if not self.manager_context:
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
"
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
]
|
|
17883
|
+
init_msg = {
|
|
17884
|
+
"role": "system",
|
|
17885
|
+
"content": (
|
|
17886
|
+
"Manager context initialized. Delegate by reading blackboard and assigning short slices.\n"
|
|
17887
|
+
f"{language_note}"
|
|
17888
|
+
),
|
|
17889
|
+
"ts": now_ts(),
|
|
17890
|
+
"agent_role": "manager",
|
|
17891
|
+
}
|
|
17892
|
+
self.manager_context = [init_msg]
|
|
17893
|
+
self.agent_messages.append(init_msg)
|
|
17018
17894
|
if not self._agent_context("developer"):
|
|
17019
17895
|
self._append_agent_context_message(
|
|
17020
17896
|
"developer",
|
|
@@ -18340,6 +19216,7 @@ class SessionState:
|
|
|
18340
19216
|
ctx = self._agent_context(role_key)
|
|
18341
19217
|
if not ctx:
|
|
18342
19218
|
return {"status": "skip", "reason": "empty-context", "role": role_key}
|
|
19219
|
+
self._microcompact_agent_messages(ctx)
|
|
18343
19220
|
with self.lock:
|
|
18344
19221
|
self.current_phase = f"agent:{role_key}:model-call"
|
|
18345
19222
|
self.current_tool_name = ""
|
|
@@ -18348,7 +19225,7 @@ class SessionState:
|
|
|
18348
19225
|
ctx,
|
|
18349
19226
|
tools=self._tools_for_agent(role_key),
|
|
18350
19227
|
system=self._agent_role_system_prompt(role_key),
|
|
18351
|
-
max_tokens=
|
|
19228
|
+
max_tokens=self.max_output_tokens,
|
|
18352
19229
|
think=False,
|
|
18353
19230
|
stream_thinking=False,
|
|
18354
19231
|
on_thinking_chunk=self._append_live_thinking,
|
|
@@ -18694,10 +19571,23 @@ class SessionState:
|
|
|
18694
19571
|
media_inputs_pool=media_inputs_pool,
|
|
18695
19572
|
media_seen_ts_by_role=media_seen_ts_by_role,
|
|
18696
19573
|
)
|
|
18697
|
-
route
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
19574
|
+
# AgentBus fast-route: skip manager if a valid worker handoff is pending
|
|
19575
|
+
bus_route = self._drain_agentbus_fast_route()
|
|
19576
|
+
if bus_route:
|
|
19577
|
+
target = bus_route["to"]
|
|
19578
|
+
instruction = trim(str(bus_route.get("payload", "") or ""), 1400)
|
|
19579
|
+
route = {
|
|
19580
|
+
"target": target,
|
|
19581
|
+
"instruction": instruction,
|
|
19582
|
+
"source": "agentbus-direct",
|
|
19583
|
+
"is_mandatory": False,
|
|
19584
|
+
"executor_mode": False,
|
|
19585
|
+
}
|
|
19586
|
+
else:
|
|
19587
|
+
route = self._manager_delegate_turn(
|
|
19588
|
+
pinned_selection=pinned_selection,
|
|
19589
|
+
media_inputs_round=manager_media_inputs,
|
|
19590
|
+
)
|
|
18701
19591
|
target = str(route.get("target", "") or "").strip().lower()
|
|
18702
19592
|
instruction = trim(str(route.get("instruction", "") or "").strip(), 1400)
|
|
18703
19593
|
if compact_mode and target in AGENT_ROLES:
|
|
@@ -18738,6 +19628,23 @@ class SessionState:
|
|
|
18738
19628
|
media_inputs_round=role_media_inputs,
|
|
18739
19629
|
)
|
|
18740
19630
|
self._blackboard_update_from_worker_step(role, step)
|
|
19631
|
+
# ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
|
|
19632
|
+
agent_text = self._latest_agent_assistant_text(role)
|
|
19633
|
+
if (
|
|
19634
|
+
agent_text
|
|
19635
|
+
and self._looks_like_conclusive_reply(agent_text)
|
|
19636
|
+
and not self.todo.has_open_items()
|
|
19637
|
+
and not self._manager_has_error_log(self._ensure_blackboard())
|
|
19638
|
+
):
|
|
19639
|
+
self._blackboard_mark_approved(
|
|
19640
|
+
f"conclusive reply from {role}: auto-finish", role
|
|
19641
|
+
)
|
|
19642
|
+
self._mark_all_done_silently(f"conclusive reply from {role}")
|
|
19643
|
+
self._emit(
|
|
19644
|
+
"status",
|
|
19645
|
+
{"summary": f"agent '{role}' gave conclusive reply; finishing run"},
|
|
19646
|
+
)
|
|
19647
|
+
break
|
|
18741
19648
|
board_after = self._ensure_blackboard()
|
|
18742
19649
|
board_after_fp = self._watchdog_state_fingerprint(board_after)
|
|
18743
19650
|
wd_event = self._watchdog_process_worker_step(
|
|
@@ -18786,6 +19693,7 @@ class SessionState:
|
|
|
18786
19693
|
},
|
|
18787
19694
|
)
|
|
18788
19695
|
continue
|
|
19696
|
+
self._auto_summary_on_finish()
|
|
18789
19697
|
self._mark_all_done_silently(note)
|
|
18790
19698
|
self._emit(
|
|
18791
19699
|
"status",
|
|
@@ -18847,7 +19755,9 @@ class SessionState:
|
|
|
18847
19755
|
)
|
|
18848
19756
|
break
|
|
18849
19757
|
else:
|
|
18850
|
-
|
|
19758
|
+
summary = self._auto_summary_on_finish()
|
|
19759
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19760
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
18851
19761
|
|
|
18852
19762
|
def _multi_agent_worker(self, *, pinned_selection: str):
|
|
18853
19763
|
mode = self._effective_execution_mode()
|
|
@@ -18980,7 +19890,9 @@ class SessionState:
|
|
|
18980
19890
|
sync_index += 1
|
|
18981
19891
|
continue
|
|
18982
19892
|
else:
|
|
18983
|
-
|
|
19893
|
+
summary = self._auto_summary_on_finish()
|
|
19894
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19895
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
18984
19896
|
|
|
18985
19897
|
def _agent_worker(self):
|
|
18986
19898
|
single_role = "developer"
|
|
@@ -19205,7 +20117,7 @@ class SessionState:
|
|
|
19205
20117
|
self.messages,
|
|
19206
20118
|
tools=TOOLS,
|
|
19207
20119
|
system=self._system_prompt(),
|
|
19208
|
-
max_tokens=
|
|
20120
|
+
max_tokens=self.max_output_tokens,
|
|
19209
20121
|
think=False,
|
|
19210
20122
|
stream_thinking=False,
|
|
19211
20123
|
on_thinking_chunk=self._append_live_thinking,
|
|
@@ -19579,6 +20491,9 @@ class SessionState:
|
|
|
19579
20491
|
},
|
|
19580
20492
|
)
|
|
19581
20493
|
continue
|
|
20494
|
+
# 对简单查询(非工程任务)限制自动继续预算
|
|
20495
|
+
if auto_continue_budget > 8 and not self._is_long_running_engineering_context():
|
|
20496
|
+
auto_continue_budget = min(auto_continue_budget, 8)
|
|
19582
20497
|
can_continue = auto_continue_budget > 0 and (
|
|
19583
20498
|
todo_blocking or self._looks_like_incomplete_reply(text)
|
|
19584
20499
|
)
|
|
@@ -20080,7 +20995,9 @@ class SessionState:
|
|
|
20080
20995
|
if stop_due_to_repeated_tool_loop or stop_due_to_hard_break or stop_due_to_finish_task:
|
|
20081
20996
|
break
|
|
20082
20997
|
else:
|
|
20083
|
-
|
|
20998
|
+
summary = self._auto_summary_on_finish()
|
|
20999
|
+
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
21000
|
+
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
20084
21001
|
except CircuitBreakerTriggered as exc:
|
|
20085
21002
|
note = trim(str(exc), 320) or "Circuit breaker triggered."
|
|
20086
21003
|
self._emit("status", {"summary": f"hard-stop: {note}"})
|
|
@@ -20446,6 +21363,107 @@ class SessionState:
|
|
|
20446
21363
|
bio.seek(0)
|
|
20447
21364
|
return bio.read()
|
|
20448
21365
|
|
|
21366
|
+
def export_conversation_md(self) -> str:
|
|
21367
|
+
snap = self.snapshot()
|
|
21368
|
+
title = snap.get("title") or snap.get("id") or "Session"
|
|
21369
|
+
lines = [
|
|
21370
|
+
f"# {title}",
|
|
21371
|
+
"",
|
|
21372
|
+
f"- Session: `{snap.get('id', '')}`",
|
|
21373
|
+
f"- Model: `{snap.get('model', '')}`",
|
|
21374
|
+
f"- Created: {_fmt_export_ts(snap.get('created_at', 0))}",
|
|
21375
|
+
"",
|
|
21376
|
+
"---",
|
|
21377
|
+
"",
|
|
21378
|
+
]
|
|
21379
|
+
for row in snap.get("conversation_feed", []):
|
|
21380
|
+
role = str(row.get("role", "system"))
|
|
21381
|
+
ts = row.get("ts", 0)
|
|
21382
|
+
time_str = _fmt_export_ts(ts)
|
|
21383
|
+
text = str(row.get("text", ""))
|
|
21384
|
+
thinking = str(row.get("thinking", "") or "")
|
|
21385
|
+
row_type = str(row.get("type", "message"))
|
|
21386
|
+
agent = str(row.get("agent_role", "") or "")
|
|
21387
|
+
header = f"**[{role}]**"
|
|
21388
|
+
if agent:
|
|
21389
|
+
header += f" _{agent}_"
|
|
21390
|
+
if row_type not in ("message", ""):
|
|
21391
|
+
header += f" `{row_type}`"
|
|
21392
|
+
if time_str:
|
|
21393
|
+
header += f" <sub>{time_str}</sub>"
|
|
21394
|
+
lines.append(header)
|
|
21395
|
+
lines.append("")
|
|
21396
|
+
if thinking:
|
|
21397
|
+
lines.append("<details><summary>thinking</summary>")
|
|
21398
|
+
lines.append("")
|
|
21399
|
+
lines.append(thinking)
|
|
21400
|
+
lines.append("")
|
|
21401
|
+
lines.append("</details>")
|
|
21402
|
+
lines.append("")
|
|
21403
|
+
if text:
|
|
21404
|
+
lines.append(text)
|
|
21405
|
+
lines.append("")
|
|
21406
|
+
lines.append("---")
|
|
21407
|
+
lines.append("")
|
|
21408
|
+
return "\n".join(lines)
|
|
21409
|
+
|
|
21410
|
+
def export_conversation_pdf(self) -> bytes:
|
|
21411
|
+
md_text = self.export_conversation_md()
|
|
21412
|
+
return _text_to_minimal_pdf(md_text)
|
|
21413
|
+
|
|
21414
|
+
def _conversation_to_html(self) -> str:
|
|
21415
|
+
snap = self.snapshot()
|
|
21416
|
+
title = _html_esc(snap.get("title") or snap.get("id") or "Session")
|
|
21417
|
+
model = _html_esc(snap.get("model", ""))
|
|
21418
|
+
rows_html = []
|
|
21419
|
+
for row in snap.get("conversation_feed", []):
|
|
21420
|
+
role = str(row.get("role", "system"))
|
|
21421
|
+
ts = row.get("ts", 0)
|
|
21422
|
+
time_str = _fmt_export_ts(ts)
|
|
21423
|
+
text = str(row.get("text", ""))
|
|
21424
|
+
thinking = str(row.get("thinking", "") or "")
|
|
21425
|
+
bg = "#e8f4fd" if role == "user" else ("#f0f0f0" if role == "assistant" else "#fff9e6")
|
|
21426
|
+
block = f'<div style="background:{bg};border-radius:8px;padding:10px 14px;margin:6px 0">'
|
|
21427
|
+
block += f'<div style="font-weight:bold;font-size:13px;color:#555;margin-bottom:4px">[{_html_esc(role)}] {_html_esc(time_str)}</div>'
|
|
21428
|
+
if thinking:
|
|
21429
|
+
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>'
|
|
21430
|
+
if text:
|
|
21431
|
+
block += f'<pre style="white-space:pre-wrap;font-size:13px;margin:0">{_html_esc(text)}</pre>'
|
|
21432
|
+
block += '</div>'
|
|
21433
|
+
rows_html.append(block)
|
|
21434
|
+
body = "\n".join(rows_html)
|
|
21435
|
+
return f"""<!DOCTYPE html>
|
|
21436
|
+
<html><head><meta charset="utf-8"><title>{title}</title>
|
|
21437
|
+
<style>body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:860px;margin:0 auto;padding:20px;background:#fff}}
|
|
21438
|
+
h1{{font-size:20px;margin-bottom:4px}}
|
|
21439
|
+
.meta{{color:#888;font-size:13px;margin-bottom:16px}}</style></head>
|
|
21440
|
+
<body><h1>{title}</h1><div class="meta">Model: {model}</div>
|
|
21441
|
+
{body}
|
|
21442
|
+
</body></html>"""
|
|
21443
|
+
|
|
21444
|
+
def export_conversation_image(self) -> bytes:
|
|
21445
|
+
html_content = self._conversation_to_html()
|
|
21446
|
+
try:
|
|
21447
|
+
from playwright.sync_api import sync_playwright
|
|
21448
|
+
with sync_playwright() as p:
|
|
21449
|
+
browser = p.chromium.launch()
|
|
21450
|
+
page = browser.new_page(viewport={"width": 860, "height": 800})
|
|
21451
|
+
page.set_content(html_content)
|
|
21452
|
+
page.wait_for_load_state("networkidle")
|
|
21453
|
+
img = page.screenshot(full_page=True, type="png")
|
|
21454
|
+
browser.close()
|
|
21455
|
+
return img
|
|
21456
|
+
except ImportError:
|
|
21457
|
+
pass
|
|
21458
|
+
except Exception:
|
|
21459
|
+
pass
|
|
21460
|
+
try:
|
|
21461
|
+
import imgkit
|
|
21462
|
+
return imgkit.from_string(html_content, False, options={"width": "860", "encoding": "UTF-8"})
|
|
21463
|
+
except ImportError:
|
|
21464
|
+
pass
|
|
21465
|
+
raise RuntimeError("Image export requires playwright or imgkit. Install: pip install playwright && playwright install chromium")
|
|
21466
|
+
|
|
20449
21467
|
class SessionManager:
|
|
20450
21468
|
def __init__(
|
|
20451
21469
|
self,
|
|
@@ -20471,6 +21489,7 @@ class SessionManager:
|
|
|
20471
21489
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
20472
21490
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
20473
21491
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
21492
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
20474
21493
|
run_finished_callback=None,
|
|
20475
21494
|
):
|
|
20476
21495
|
self.root = root
|
|
@@ -20493,6 +21512,7 @@ class SessionManager:
|
|
|
20493
21512
|
MIN_AGENT_ROUNDS,
|
|
20494
21513
|
min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
|
|
20495
21514
|
)
|
|
21515
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
20496
21516
|
self.max_run_seconds = normalize_timeout_seconds(
|
|
20497
21517
|
max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
|
|
20498
21518
|
minimum=MIN_RUN_TIMEOUT_SECONDS,
|
|
@@ -20839,6 +21859,7 @@ class SessionManager:
|
|
|
20839
21859
|
arbiter_max_tokens=self.arbiter_max_tokens,
|
|
20840
21860
|
arbiter_temperature=self.arbiter_temperature,
|
|
20841
21861
|
execution_mode=self.execution_mode,
|
|
21862
|
+
max_output_tokens=self.max_output_tokens,
|
|
20842
21863
|
ui_language=self.user_language,
|
|
20843
21864
|
js_lib_root=self.js_lib_root,
|
|
20844
21865
|
owner_user_id=self.user_id,
|
|
@@ -20879,6 +21900,7 @@ class SessionManager:
|
|
|
20879
21900
|
arbiter_max_tokens=self.arbiter_max_tokens,
|
|
20880
21901
|
arbiter_temperature=self.arbiter_temperature,
|
|
20881
21902
|
execution_mode=self.execution_mode,
|
|
21903
|
+
max_output_tokens=self.max_output_tokens,
|
|
20882
21904
|
ui_language=self.user_language,
|
|
20883
21905
|
js_lib_root=self.js_lib_root,
|
|
20884
21906
|
owner_user_id=self.user_id,
|
|
@@ -21291,7 +22313,7 @@ window.MathJax={
|
|
|
21291
22313
|
<button id="applyModelBtn" class="subtle">Apply Model</button>
|
|
21292
22314
|
<button id="importConfigBtn" class="subtle">Upload LLM.config.json</button>
|
|
21293
22315
|
<input id="configInput" type="file" accept=".json,application/json" style="display:none">
|
|
21294
|
-
<a id="downloadBtn" href="#"
|
|
22316
|
+
<a id="downloadBtn" href="#">Open Skills Studio</a>
|
|
21295
22317
|
</div>
|
|
21296
22318
|
</header>
|
|
21297
22319
|
<div class="status-cards" id="topStats"></div>
|
|
@@ -21320,7 +22342,15 @@ window.MathJax={
|
|
|
21320
22342
|
<button id="interruptBtn" class="subtle">Interrupt</button>
|
|
21321
22343
|
<button id="compactBtn" class="subtle">Compact</button>
|
|
21322
22344
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
21323
|
-
<
|
|
22345
|
+
<div class="export-dropdown" style="position:relative;display:inline-block">
|
|
22346
|
+
<button id="exportMenuBtn" class="subtle">Export ▾</button>
|
|
22347
|
+
<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">
|
|
22348
|
+
<a id="downloadSessionBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export ZIP</a>
|
|
22349
|
+
<a id="exportMdBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Markdown</a>
|
|
22350
|
+
<a id="exportPdfBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export PDF</a>
|
|
22351
|
+
<a id="exportPngBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Image</a>
|
|
22352
|
+
</div>
|
|
22353
|
+
</div>
|
|
21324
22354
|
<div id="ctxLive" class="ctx-live" title="Remaining context budget">
|
|
21325
22355
|
<span class="ctx-live-dot"></span>
|
|
21326
22356
|
<span id="ctxLiveText" class="mono">ctx_left=-</span>
|
|
@@ -21394,6 +22424,7 @@ button,a{border:1px solid var(--line);padding:10px 14px;border-radius:12px;backg
|
|
|
21394
22424
|
button:hover,a:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(15,27,45,.08)}
|
|
21395
22425
|
#sendBtn,#newSessionBtn{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#fff;border:0}
|
|
21396
22426
|
.subtle{background:#f6f8fa}
|
|
22427
|
+
.export-item:hover{background:#f0f4f8}
|
|
21397
22428
|
.actions select{padding:10px 12px;border-radius:12px;border:1px solid var(--line);background:#fff;min-width:160px}
|
|
21398
22429
|
.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
22430
|
.danger{color:var(--warn);border-color:#f3c0c0}
|
|
@@ -21508,7 +22539,7 @@ main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px)
|
|
|
21508
22539
|
.msg-md blockquote{margin:.5rem 0;padding:.4rem .6rem;border-left:3px solid #9db8e8;background:#eef4ff;border-radius:6px;color:#27446f}
|
|
21509
22540
|
.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
22541
|
.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}
|
|
22542
|
+
.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
22543
|
.msg-md .md-table{margin:.5rem 0;border-collapse:collapse;max-width:100%;width:100%;display:block;overflow:auto;background:#fff}
|
|
21513
22544
|
.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
22545
|
.msg-md .md-table th{background:#f5f8fc;font-weight:700}
|
|
@@ -21618,7 +22649,7 @@ h3{font-size:.96rem;margin:10px 0 6px}
|
|
|
21618
22649
|
|
|
21619
22650
|
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
22651
|
const MD_CACHE=new Map();
|
|
21621
|
-
const MD_CACHE_MAX=
|
|
22652
|
+
const MD_CACHE_MAX=280;
|
|
21622
22653
|
const STATIC_UI=((new URLSearchParams(location.search)).get('static_ui')==='1');
|
|
21623
22654
|
const SNAPSHOT_DELAY_VISIBLE_MS=300;
|
|
21624
22655
|
const SNAPSHOT_DELAY_HIDDEN_MS=2400;
|
|
@@ -21633,30 +22664,30 @@ const CHAT_SCROLL_RENDER_THROTTLE_MS=70;
|
|
|
21633
22664
|
const CHAT_SCROLL_SYNC_DEBOUNCE_MS=260;
|
|
21634
22665
|
const CHAT_SCROLL_SETTLE_MS=620;
|
|
21635
22666
|
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=
|
|
22667
|
+
const DELTA_MAX_FEED=300;
|
|
22668
|
+
const DELTA_MAX_MESSAGES=300;
|
|
22669
|
+
const DELTA_MAX_ACTIVITY=80;
|
|
22670
|
+
const DELTA_MAX_OPERATIONS=160;
|
|
21640
22671
|
const DELTA_MAX_UPLOADS=40;
|
|
21641
22672
|
const DELTA_WATCHDOG_INTERVAL_MS=1800;
|
|
21642
22673
|
const DELTA_WATCHDOG_STALL_MS=9000;
|
|
21643
22674
|
const MARKDOWN_WORKER_MIN_CHARS=800;
|
|
21644
22675
|
const MARKDOWN_WORKER_MAX_PENDING=96;
|
|
21645
22676
|
const MARKDOWN_WORKER_REQ_TTL_MS=45000;
|
|
21646
|
-
const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:
|
|
22677
|
+
const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:600,poolByKind:Object.create(null),poolSize:0,poolMax:180};
|
|
21647
22678
|
const RENDER_EVT_TYPES=new Set(['render_frame','render_bridge']);
|
|
21648
|
-
const RENDER_QUEUE_MAX=
|
|
22679
|
+
const RENDER_QUEUE_MAX=80;
|
|
21649
22680
|
const RENDER_META_MIN_INTERVAL_MS=180;
|
|
21650
22681
|
const RENDER={queue:[],raf:0,canvas:null,ctx:null,lastSeq:0,lastPaintAt:0,lastMetaAt:0,lastSummary:'',hideTimer:0,imgTicket:0};
|
|
21651
22682
|
const CODE_PREVIEW_VIRT_THRESHOLD=1800;
|
|
21652
22683
|
const CODE_PREVIEW_VIRT_EST_ROW_PX=24;
|
|
21653
22684
|
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']);
|
|
22685
|
+
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
22686
|
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'};
|
|
22687
|
+
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
22688
|
const CODE_LANG_BY_NAME={'dockerfile':'shell','makefile':'makefile','cmakelists.txt':'cmake','justfile':'makefile','gemfile':'ruby','rakefile':'ruby','pipfile':'ini','requirements.txt':'ini'};
|
|
21658
22689
|
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'])};
|
|
22690
|
+
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
22691
|
S.staticMode=STATIC_UI;
|
|
21661
22692
|
const COMPACT_AUTO_REFRESH_COUNT=3;
|
|
21662
22693
|
const COMPACT_AUTO_REFRESH_INTERVAL_MS=260;
|
|
@@ -21820,18 +22851,18 @@ function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText'
|
|
|
21820
22851
|
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
22852
|
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
22853
|
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(
|
|
22854
|
+
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
22855
|
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=
|
|
22856
|
+
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
22857
|
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
22858
|
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
22859
|
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
22860
|
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
22861
|
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(
|
|
22862
|
+
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
22863
|
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
22864
|
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(
|
|
22865
|
+
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
22866
|
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
22867
|
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
22868
|
function _deltaAdoptAgentRole(data){if(!_deltaEnsureSnapshot())return'';const role=_chatVirtAgentRoleKey(data?.agent_role);if(!role)return'';S.snap.agent_active_role=role;return role}
|
|
@@ -22016,7 +23047,7 @@ function onRuntimeEvent(evt){
|
|
|
22016
23047
|
if(seqState.gap)return{handled:true,needsSnapshot:true};
|
|
22017
23048
|
const typ=String(evt.type||'');
|
|
22018
23049
|
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(
|
|
23050
|
+
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}}
|
|
22020
23051
|
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));_deltaScheduleRender({boards:true,sessions:true});return{handled:true,needsSnapshot:false}}
|
|
22021
23052
|
return _deltaApplyRuntimeEvent(evt);
|
|
22022
23053
|
}
|
|
@@ -22047,7 +23078,7 @@ function _deltaStartWatchdog(){
|
|
|
22047
23078
|
};
|
|
22048
23079
|
S.deltaWatchdogTimer=setTimeout(tick,DELTA_WATCHDOG_INTERVAL_MS);
|
|
22049
23080
|
}
|
|
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')
|
|
23081
|
+
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
23082
|
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
23083
|
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
23084
|
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 +23097,12 @@ function _scrollContainerToNodeCenter(container,target){
|
|
|
22066
23097
|
}
|
|
22067
23098
|
function _bindNestedScrollGuards(root){
|
|
22068
23099
|
if(!root)return;
|
|
23100
|
+
const SEL='.msg-diff-shell,.msg-code-shell,.preview-code-scroll,.md-code';
|
|
22069
23101
|
const nodes=[];
|
|
22070
|
-
if(root.matches&&root.matches(
|
|
23102
|
+
if(root.matches&&root.matches(SEL)){
|
|
22071
23103
|
nodes.push(root);
|
|
22072
23104
|
}
|
|
22073
|
-
for(const n of root.querySelectorAll(
|
|
23105
|
+
for(const n of root.querySelectorAll(SEL)){
|
|
22074
23106
|
nodes.push(n);
|
|
22075
23107
|
}
|
|
22076
23108
|
const markManualCenterOff=(node)=>{
|
|
@@ -22087,6 +23119,17 @@ function _bindNestedScrollGuards(root){
|
|
|
22087
23119
|
if(key)S.diffCenterDisabled[key]=1;
|
|
22088
23120
|
}
|
|
22089
23121
|
};
|
|
23122
|
+
const markChatUserScrolling=()=>{
|
|
23123
|
+
const chatEl=E('chat');
|
|
23124
|
+
if(!chatEl)return;
|
|
23125
|
+
const now=Date.now();
|
|
23126
|
+
chatEl._virtManualUnlockTs=Math.max(
|
|
23127
|
+
Number(chatEl._virtManualUnlockTs||0),
|
|
23128
|
+
now+CHAT_SCROLL_LOCK_MS
|
|
23129
|
+
);
|
|
23130
|
+
S.follow.chat=false;
|
|
23131
|
+
chatEl._virtAutoFollowPaused=true;
|
|
23132
|
+
};
|
|
22090
23133
|
for(const node of nodes){
|
|
22091
23134
|
if(!node||node._nestedScrollGuardBound)continue;
|
|
22092
23135
|
node._nestedScrollGuardBound=true;
|
|
@@ -22097,16 +23140,38 @@ function _bindNestedScrollGuards(root){
|
|
|
22097
23140
|
const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
|
|
22098
23141
|
const top=Number(node.scrollTop||0);
|
|
22099
23142
|
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);
|
|
23143
|
+
const canY=(dy<0&&top>0.5)||(dy>0&&top<maxTop-0.5);
|
|
23144
|
+
const canX=(dx<0&&left>0.5)||(dx>0&&left<maxLeft-0.5);
|
|
22102
23145
|
markManualCenterOff(node);
|
|
22103
23146
|
if(canY||canX){
|
|
22104
23147
|
ev.stopPropagation();
|
|
23148
|
+
markChatUserScrolling();
|
|
22105
23149
|
}
|
|
23150
|
+
},{passive:false});
|
|
23151
|
+
node.addEventListener('mousedown',()=>{markManualCenterOff(node);markChatUserScrolling();},{passive:true});
|
|
23152
|
+
node.addEventListener('touchstart',ev=>{
|
|
23153
|
+
markManualCenterOff(node);
|
|
23154
|
+
markChatUserScrolling();
|
|
23155
|
+
node._touchStartY=Number(ev.touches?.[0]?.clientY||0);
|
|
23156
|
+
node._touchStartX=Number(ev.touches?.[0]?.clientX||0);
|
|
22106
23157
|
},{passive:true});
|
|
22107
|
-
node.addEventListener('
|
|
22108
|
-
|
|
22109
|
-
|
|
23158
|
+
node.addEventListener('touchmove',ev=>{
|
|
23159
|
+
markManualCenterOff(node);
|
|
23160
|
+
const curY=Number(ev.touches?.[0]?.clientY||0);
|
|
23161
|
+
const curX=Number(ev.touches?.[0]?.clientX||0);
|
|
23162
|
+
const dy=curY-(node._touchStartY||0);
|
|
23163
|
+
const dx=curX-(node._touchStartX||0);
|
|
23164
|
+
const maxTop=Math.max(0,Number(node.scrollHeight||0)-Number(node.clientHeight||0));
|
|
23165
|
+
const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
|
|
23166
|
+
const top=Number(node.scrollTop||0);
|
|
23167
|
+
const left=Number(node.scrollLeft||0);
|
|
23168
|
+
const canY=(dy>0&&top>0.5)||(dy<0&&top<maxTop-0.5);
|
|
23169
|
+
const canX=(dx>0&&left>0.5)||(dx<0&&left<maxLeft-0.5);
|
|
23170
|
+
if(canY||canX){
|
|
23171
|
+
ev.stopPropagation();
|
|
23172
|
+
}
|
|
23173
|
+
markChatUserScrolling();
|
|
23174
|
+
},{passive:false});
|
|
22110
23175
|
}
|
|
22111
23176
|
}
|
|
22112
23177
|
function _centerDiffShellToHotspot(root){
|
|
@@ -22136,8 +23201,7 @@ function _centerDiffShellToHotspot(root){
|
|
|
22136
23201
|
const target=lines[bestCenter];
|
|
22137
23202
|
if(!target)return;
|
|
22138
23203
|
if(msgKey)shell.setAttribute('data-centered-key',msgKey);
|
|
22139
|
-
|
|
22140
|
-
if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
|
|
23204
|
+
try{_scrollContainerToNodeCenter(shell,target);if(msgKey)S.diffCenteredDone[msgKey]=1;}catch(_){}
|
|
22141
23205
|
}
|
|
22142
23206
|
function splitTableRow(line){const src=String(line||'').trim().replace(/^\\|/,'').replace(/\\|$/,'');if(!src)return[];return src.split('|').map(x=>String(x||'').trim())}
|
|
22143
23207
|
function isTableSeparator(line){const cells=splitTableRow(line);if(!cells.length)return false;return cells.every(cell=>/^:?-{3,}:?$/.test(cell))}
|
|
@@ -22509,7 +23573,7 @@ function _previewRenderStageSelector(tab,stages,selectedReq,payload=null){
|
|
|
22509
23573
|
stat.textContent=`stage ${idx}/${total} · +${add}/-${del}${lineTail}`;
|
|
22510
23574
|
}
|
|
22511
23575
|
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:
|
|
23576
|
+
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
23577
|
function _codeWordSet(lang){return CODE_KEYWORDS[String(lang||'default')]||CODE_KEYWORDS.default}
|
|
22514
23578
|
function _isWordStart(ch){return /[A-Za-z_$]/.test(ch)}
|
|
22515
23579
|
function _isWordChar(ch){return /[A-Za-z0-9_$]/.test(ch)}
|
|
@@ -22913,8 +23977,7 @@ function _scrollCodePreviewToAnchor(body,anchorLine){
|
|
|
22913
23977
|
target=body.querySelector('.code-row.code-add,.code-row.code-delete')||rows[0];
|
|
22914
23978
|
}
|
|
22915
23979
|
if(!target)return;
|
|
22916
|
-
|
|
22917
|
-
if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
|
|
23980
|
+
try{_scrollContainerToNodeCenter(scrollWrap,target);if(previewKey)S.previewCenteredDone[previewKey]=1;}catch(_){}
|
|
22918
23981
|
}
|
|
22919
23982
|
async function _renderCodePreviewTab(tab,body,forceReload=false){
|
|
22920
23983
|
const ticket=String(++S.previewNonce);
|
|
@@ -23032,7 +24095,7 @@ function _chatVirtRowKey(row,idx){const r=row||{};const txt=String(r.text||'');c
|
|
|
23032
24095
|
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
24096
|
function _chatVirtLiveRunText(label,elapsed){return `${t('running')} · ${_chatVirtFormatElapsed(elapsed)}`}
|
|
23034
24097
|
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}
|
|
24098
|
+
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
24099
|
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
24100
|
function _chatVirtCollectRows(){
|
|
23038
24101
|
const feed=Array.isArray(S.snap?.conversation_feed)?S.snap.conversation_feed:(Array.isArray(S.snap?.messages)?S.snap.messages:[]);
|
|
@@ -23101,7 +24164,7 @@ function _chatVirtAcquireNode(kind){const key=String(kind||'text');const pool=_c
|
|
|
23101
24164
|
function _chatVirtReleaseNode(node){
|
|
23102
24165
|
if(!node)return;
|
|
23103
24166
|
const key=String(node.getAttribute('data-pool-kind')||'text');
|
|
23104
|
-
if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||
|
|
24167
|
+
if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||180))return;
|
|
23105
24168
|
if(S.mathObserver){
|
|
23106
24169
|
try{S.mathObserver.unobserve(node)}catch(_){}
|
|
23107
24170
|
}
|
|
@@ -23316,6 +24379,70 @@ function _chatVirtBuildMessageNode(m){
|
|
|
23316
24379
|
return d;
|
|
23317
24380
|
}
|
|
23318
24381
|
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}}
|
|
24382
|
+
function _chatVirtReuseWindow(chatEl,rows,top,bottom){
|
|
24383
|
+
if(!chatEl||!Array.isArray(rows)||!rows.length)return null;
|
|
24384
|
+
const prevRows=Array.isArray(chatEl._virtLastRows)?chatEl._virtLastRows:[];
|
|
24385
|
+
const prevStart=Number(chatEl._virtLastWinStart||-1);
|
|
24386
|
+
const prevEnd=Number(chatEl._virtLastWinEnd||-1);
|
|
24387
|
+
const prevTopOffset=Number(chatEl._virtLastTopOffset);
|
|
24388
|
+
const prevEndOffset=Number(chatEl._virtLastEndOffset);
|
|
24389
|
+
if(prevStart<0||prevEnd<=prevStart)return null;
|
|
24390
|
+
if(!Number.isFinite(prevTopOffset)||!Number.isFinite(prevEndOffset)||prevEndOffset<=prevTopOffset)return null;
|
|
24391
|
+
if(prevRows.length!==rows.length)return null;
|
|
24392
|
+
const prevFirstKey=String(prevRows[prevStart]?._vk||'');
|
|
24393
|
+
const nextFirstKey=String(rows[prevStart]?._vk||'');
|
|
24394
|
+
const prevLastKey=String(prevRows[Math.max(0,prevEnd-1)]?._vk||'');
|
|
24395
|
+
const nextLastKey=String(rows[Math.max(0,prevEnd-1)]?._vk||'');
|
|
24396
|
+
if(prevFirstKey!==nextFirstKey||prevLastKey!==nextLastKey)return null;
|
|
24397
|
+
const viewport=Math.max(0,bottom-top);
|
|
24398
|
+
const innerPad=Math.max(120,Math.min(Math.round(CHAT_VIRT.overscanPx*0.45),Math.round(viewport*0.35)));
|
|
24399
|
+
if((prevEndOffset-prevTopOffset)<(viewport+(innerPad*2)))return null;
|
|
24400
|
+
const safeTop=prevTopOffset+innerPad;
|
|
24401
|
+
const safeBottom=prevEndOffset-innerPad;
|
|
24402
|
+
if(top>=safeTop&&bottom<=safeBottom){
|
|
24403
|
+
return {start:prevStart,end:prevEnd,topOffset:prevTopOffset,endOffset:prevEndOffset};
|
|
24404
|
+
}
|
|
24405
|
+
return null;
|
|
24406
|
+
}
|
|
24407
|
+
function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
|
|
24408
|
+
function _chatVirtFindRenderedNode(chatEl,key){
|
|
24409
|
+
if(!chatEl||!key)return null;
|
|
24410
|
+
for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
|
|
24411
|
+
if(String(node.getAttribute('data-vk')||'')===String(key||''))return node;
|
|
24412
|
+
}
|
|
24413
|
+
return null;
|
|
24414
|
+
}
|
|
24415
|
+
function _chatVirtCaptureAnchor(chatEl){
|
|
24416
|
+
if(!chatEl)return null;
|
|
24417
|
+
const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
|
|
24418
|
+
let fallback=null;
|
|
24419
|
+
for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
|
|
24420
|
+
const key=String(node.getAttribute('data-vk')||'').trim();
|
|
24421
|
+
if(!key)continue;
|
|
24422
|
+
const rect=node.getBoundingClientRect();
|
|
24423
|
+
const top=Number(rect.top||0)-viewportTop;
|
|
24424
|
+
const bottom=Number(rect.bottom||0)-viewportTop;
|
|
24425
|
+
const anchor={key:key,offset:top};
|
|
24426
|
+
if(!fallback)fallback=anchor;
|
|
24427
|
+
if(bottom>1)return anchor;
|
|
24428
|
+
}
|
|
24429
|
+
return fallback;
|
|
24430
|
+
}
|
|
24431
|
+
function _chatVirtRestoreAnchor(chatEl,anchor){
|
|
24432
|
+
if(!chatEl||!anchor||!anchor.key)return false;
|
|
24433
|
+
const node=_chatVirtFindRenderedNode(chatEl,anchor.key);
|
|
24434
|
+
if(!node)return false;
|
|
24435
|
+
const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
|
|
24436
|
+
const rect=node.getBoundingClientRect();
|
|
24437
|
+
const currentOffset=Number(rect.top||0)-viewportTop;
|
|
24438
|
+
const delta=currentOffset-Number(anchor.offset||0);
|
|
24439
|
+
if(Math.abs(delta)<0.75)return true;
|
|
24440
|
+
const maxTop=Math.max(0,Number(chatEl.scrollHeight||0)-Number(chatEl.clientHeight||0));
|
|
24441
|
+
const target=Math.max(0,Math.min(Number(chatEl.scrollTop||0)+delta,maxTop));
|
|
24442
|
+
if(Math.abs(target-Number(chatEl.scrollTop||0))<0.75)return true;
|
|
24443
|
+
chatEl.scrollTop=target;
|
|
24444
|
+
return true;
|
|
24445
|
+
}
|
|
23319
24446
|
function _chatVirtBindScroll(chatEl){
|
|
23320
24447
|
if(chatEl._virtBound)return;
|
|
23321
24448
|
chatEl._virtBound=true;
|
|
@@ -23385,28 +24512,11 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23385
24512
|
const now=Date.now();
|
|
23386
24513
|
chatEl._virtLastWheelTs=now;
|
|
23387
24514
|
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
24515
|
if(dy<0){
|
|
23394
|
-
markManual(CHAT_SCROLL_LOCK_MS);
|
|
23395
|
-
S.follow.chat=false;
|
|
23396
|
-
return;
|
|
23397
|
-
}
|
|
23398
|
-
if(!atBottomBefore){
|
|
23399
|
-
markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.45));
|
|
23400
24516
|
S.follow.chat=false;
|
|
23401
24517
|
return;
|
|
23402
24518
|
}
|
|
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
|
-
}
|
|
24519
|
+
if(nearBottom(chatEl,6))S.follow.chat=true;
|
|
23410
24520
|
},{passive:true});
|
|
23411
24521
|
chatEl.addEventListener('mousedown',()=>{markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.9))},{passive:true});
|
|
23412
24522
|
chatEl.addEventListener('touchstart',()=>{markTouchStart(CHAT_TOUCH_SCROLL_LOCK_MS)},{passive:true});
|
|
@@ -23420,9 +24530,7 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23420
24530
|
chatEl._virtScrollDirection=(curTop>prevTop)?1:((curTop<prevTop)?-1:0);
|
|
23421
24531
|
chatEl._virtLastScrollTop=curTop;
|
|
23422
24532
|
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){
|
|
24533
|
+
if(atBottom){
|
|
23426
24534
|
S.follow.chat=true;
|
|
23427
24535
|
chatEl._virtManualUnlockTs=0;
|
|
23428
24536
|
chatEl._virtInputUnlockTs=0;
|
|
@@ -23433,9 +24541,6 @@ function _chatVirtBindScroll(chatEl){
|
|
|
23433
24541
|
if(S.snap?.running){
|
|
23434
24542
|
chatEl._virtAutoFollowPaused=true;
|
|
23435
24543
|
}
|
|
23436
|
-
if(!atBottom||recentUpIntent){
|
|
23437
|
-
chatEl._virtManualUnlockTs=Math.max(Number(chatEl._virtManualUnlockTs||0),now+CHAT_SCROLL_LOCK_MS);
|
|
23438
|
-
}
|
|
23439
24544
|
}
|
|
23440
24545
|
scheduleScrollRender();
|
|
23441
24546
|
});
|
|
@@ -23453,12 +24558,9 @@ function renderChat(reason='snapshot'){
|
|
|
23453
24558
|
if((!S.snap?.running)&&atBottomNow){
|
|
23454
24559
|
c._virtAutoFollowPaused=false;
|
|
23455
24560
|
}
|
|
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)));
|
|
24561
|
+
const keep=first||Boolean(S.follow.chat)||atBottomNow;
|
|
23461
24562
|
const oldScrollTop=Number(c.scrollTop||0);
|
|
24563
|
+
const anchor=(!keep&&!first)?_chatVirtCaptureAnchor(c):null;
|
|
23462
24564
|
const feedSig=String(S.lastFeedSig||feedSignature(S.snap||{}));
|
|
23463
24565
|
let rows=[];
|
|
23464
24566
|
if(reason==='scroll'&&Array.isArray(c._virtRowsCacheRows)&&String(c._virtRowsCacheSig||'')===feedSig){
|
|
@@ -23474,7 +24576,7 @@ function renderChat(reason='snapshot'){
|
|
|
23474
24576
|
const prevWinEnd=Number(c._virtLastWinEnd||-1);
|
|
23475
24577
|
const top=Math.max(0,c.scrollTop);
|
|
23476
24578
|
const bottom=top+Math.max(0,c.clientHeight||0);
|
|
23477
|
-
const win=_chatVirtFindWindow(rows,top,bottom);
|
|
24579
|
+
const win=((reason==='scroll')?_chatVirtReuseWindow(c,rows,top,bottom):null)||_chatVirtFindWindow(rows,top,bottom);
|
|
23478
24580
|
const totalKey=`${feedSig}|hv=${Number(CHAT_VIRT.heightVersion||0)}|rows=${rows.length}`;
|
|
23479
24581
|
let totalEstimated=0;
|
|
23480
24582
|
if(reason==='scroll'&&String(c._virtTotalKey||'')===totalKey){
|
|
@@ -23592,19 +24694,19 @@ function renderChat(reason='snapshot'){
|
|
|
23592
24694
|
CHAT_VIRT.heightVersion=Number(CHAT_VIRT.heightVersion||0)+1;
|
|
23593
24695
|
}
|
|
23594
24696
|
}
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23600
|
-
c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
|
|
23601
|
-
}
|
|
24697
|
+
const maxTop=Math.max(0,c.scrollHeight-c.clientHeight);
|
|
24698
|
+
if(keep){
|
|
24699
|
+
c.scrollTop=maxTop;
|
|
24700
|
+
}else if(!(anchor&&_chatVirtRestoreAnchor(c,anchor))){
|
|
24701
|
+
c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
|
|
23602
24702
|
}
|
|
23603
24703
|
c._chatHasRendered=true;
|
|
23604
24704
|
c._virtRendering=false;
|
|
23605
24705
|
c._virtLastRows=rows;
|
|
23606
24706
|
c._virtLastWinStart=win.start;
|
|
23607
24707
|
c._virtLastWinEnd=win.end;
|
|
24708
|
+
c._virtLastTopOffset=Number(win.topOffset||0);
|
|
24709
|
+
c._virtLastEndOffset=Number(win.endOffset||0);
|
|
23608
24710
|
if(hasHeightChange&&reason!=='scroll'){
|
|
23609
24711
|
if(c._virtMeasureRaf)cancelAnimationFrame(c._virtMeasureRaf);
|
|
23610
24712
|
c._virtMeasureRaf=requestAnimationFrame(()=>{c._virtMeasureRaf=0;renderChat('measure')});
|
|
@@ -23654,8 +24756,14 @@ refreshFileExplorer(false).catch(()=>{});
|
|
|
23654
24756
|
const uploads=(S.snap?.uploads||[]).slice(-8).reverse();
|
|
23655
24757
|
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
24758
|
const sessionZip=S.activeId?('/api/sessions/'+S.activeId+'/export.zip'):'#';
|
|
24759
|
+
const sessionMd=S.activeId?('/api/sessions/'+S.activeId+'/export.md'):'#';
|
|
24760
|
+
const sessionPdf=S.activeId?('/api/sessions/'+S.activeId+'/export.pdf'):'#';
|
|
24761
|
+
const sessionPng=S.activeId?('/api/sessions/'+S.activeId+'/export.png'):'#';
|
|
23657
24762
|
const dl1=E('downloadSessionBtn');
|
|
23658
|
-
|
|
24763
|
+
const dlMd=E('exportMdBtn');
|
|
24764
|
+
const dlPdf=E('exportPdfBtn');
|
|
24765
|
+
const dlPng=E('exportPngBtn');
|
|
24766
|
+
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
24767
|
renderSkillsEntryLink()}
|
|
23660
24768
|
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
24769
|
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 +24861,7 @@ function _chatVirtDebounceWhileScrolling(chatEl,timerField,fn,delayMs=CHAT_SCROL
|
|
|
23753
24861
|
if(!_chatVirtIsUserScrolling(chatEl))done();
|
|
23754
24862
|
};
|
|
23755
24863
|
chatEl[scrollEndField]=onScrollEnd;
|
|
23756
|
-
|
|
24864
|
+
chatEl.addEventListener('scrollend',onScrollEnd,{once:true,passive:true});
|
|
23757
24865
|
}
|
|
23758
24866
|
}
|
|
23759
24867
|
async function refreshSnapshot(opt={}){
|
|
@@ -23903,7 +25011,7 @@ async function compactNow(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resu
|
|
|
23903
25011
|
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
25012
|
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
25013
|
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})})})
|
|
25014
|
+
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
25015
|
"""
|
|
23908
25016
|
|
|
23909
25017
|
APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
|
|
@@ -23957,7 +25065,7 @@ SKILLS_INDEX_HTML = """<!doctype html>
|
|
|
23957
25065
|
<select id="modelSelect"></select>
|
|
23958
25066
|
<button id="applyModelBtn" class="subtle">Apply Model</button>
|
|
23959
25067
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
23960
|
-
<a id="agentLink" href="#"
|
|
25068
|
+
<a id="agentLink" href="#">Open Agent UI</a>
|
|
23961
25069
|
</div>
|
|
23962
25070
|
</header>
|
|
23963
25071
|
<div class="status-cards" id="topStats"></div>
|
|
@@ -24251,9 +25359,9 @@ function pointToSegmentDistance(px,py,x1,y1,x2,y2){const dx=x2-x1,dy=y2-y1;const
|
|
|
24251
25359
|
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
25360
|
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
25361
|
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;
|
|
25362
|
+
let flowWrapRaf=0;let flowWrapDebounce=0;
|
|
24255
25363
|
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()})}
|
|
25364
|
+
function scheduleFlowWrapAdjust(){if(flowWrapRaf)cancelAnimationFrame(flowWrapRaf);if(flowWrapDebounce)clearTimeout(flowWrapDebounce);flowWrapDebounce=setTimeout(()=>{flowWrapDebounce=0;flowWrapRaf=requestAnimationFrame(()=>{flowWrapRaf=0;adjustFlowWrapHeight()})},60)}
|
|
24257
25365
|
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
25366
|
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
25367
|
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 +25428,7 @@ class AppContext:
|
|
|
24320
25428
|
arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
|
|
24321
25429
|
arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
|
|
24322
25430
|
execution_mode: str = EXECUTION_MODE_SYNC,
|
|
25431
|
+
max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
|
|
24323
25432
|
max_user: int = 0,
|
|
24324
25433
|
max_user_sessions: int = 0,
|
|
24325
25434
|
):
|
|
@@ -24364,6 +25473,7 @@ class AppContext:
|
|
|
24364
25473
|
self.arbiter_max_tokens = max(24, min(256, int(arbiter_max_tokens or ARBITER_DEFAULT_MAX_TOKENS)))
|
|
24365
25474
|
self.arbiter_temperature = max(0.0, min(1.0, float(arbiter_temperature if arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)))
|
|
24366
25475
|
self.execution_mode = normalize_execution_mode(execution_mode, default=EXECUTION_MODE_SYNC)
|
|
25476
|
+
self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
|
|
24367
25477
|
self.skills_root = skills_root
|
|
24368
25478
|
ensure_runtime_skills(self.skills_root)
|
|
24369
25479
|
self.skills_store = SkillStore(self.skills_root)
|
|
@@ -24832,6 +25942,7 @@ class AppContext:
|
|
|
24832
25942
|
self.arbiter_max_tokens,
|
|
24833
25943
|
self.arbiter_temperature,
|
|
24834
25944
|
self.execution_mode,
|
|
25945
|
+
self.max_output_tokens,
|
|
24835
25946
|
run_finished_callback=self._on_session_run_finished,
|
|
24836
25947
|
)
|
|
24837
25948
|
self._session_mgrs[user_id] = mgr
|
|
@@ -24913,6 +26024,9 @@ class AppContext:
|
|
|
24913
26024
|
active = dict(self.global_profiles.get(self.global_active_profile_id, {}))
|
|
24914
26025
|
self._sync_global_ollama_defaults(active)
|
|
24915
26026
|
self.thinking = False
|
|
26027
|
+
cfg_max_output_tokens = cfg.get("max_output_tokens")
|
|
26028
|
+
if cfg_max_output_tokens is not None:
|
|
26029
|
+
self.max_output_tokens = max(256, int(cfg_max_output_tokens))
|
|
24916
26030
|
|
|
24917
26031
|
def normalized_profiles() -> tuple[dict[str, dict], str]:
|
|
24918
26032
|
rows: dict[str, dict] = {}
|
|
@@ -26007,6 +27121,33 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
26007
27121
|
if not sess:
|
|
26008
27122
|
return self._send_json({"error": "session not found"}, status=404)
|
|
26009
27123
|
return self._send_bytes(sess.export_bundle(), "application/zip", f"{sess.id}_session_export.zip")
|
|
27124
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.md$", path)
|
|
27125
|
+
if m:
|
|
27126
|
+
sess = mgr.get(m.group(1))
|
|
27127
|
+
if not sess:
|
|
27128
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27129
|
+
md = sess.export_conversation_md()
|
|
27130
|
+
return self._send_bytes(md.encode("utf-8"), "text/markdown; charset=utf-8", f"{sess.id}_conversation.md")
|
|
27131
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.pdf$", path)
|
|
27132
|
+
if m:
|
|
27133
|
+
sess = mgr.get(m.group(1))
|
|
27134
|
+
if not sess:
|
|
27135
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27136
|
+
try:
|
|
27137
|
+
pdf = sess.export_conversation_pdf()
|
|
27138
|
+
return self._send_bytes(pdf, "application/pdf", f"{sess.id}_conversation.pdf")
|
|
27139
|
+
except Exception as exc:
|
|
27140
|
+
return self._send_json({"error": str(exc)}, status=500)
|
|
27141
|
+
m = re.match(r"^/api/sessions/([^/]+)/export\.png$", path)
|
|
27142
|
+
if m:
|
|
27143
|
+
sess = mgr.get(m.group(1))
|
|
27144
|
+
if not sess:
|
|
27145
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
27146
|
+
try:
|
|
27147
|
+
img = sess.export_conversation_image()
|
|
27148
|
+
return self._send_bytes(img, "image/png", f"{sess.id}_conversation.png")
|
|
27149
|
+
except Exception as exc:
|
|
27150
|
+
return self._send_json({"error": str(exc)}, status=500)
|
|
26010
27151
|
return self._send_json({"error": "not found"}, status=404)
|
|
26011
27152
|
|
|
26012
27153
|
def do_POST(self):
|
|
@@ -26640,6 +27781,12 @@ def main():
|
|
|
26640
27781
|
default="",
|
|
26641
27782
|
help="Agent execution mode (single|sequential|sync). Empty means read from startup config, then fallback to sync.",
|
|
26642
27783
|
)
|
|
27784
|
+
parser.add_argument(
|
|
27785
|
+
"--max-output-tokens",
|
|
27786
|
+
default=AGENT_MAX_OUTPUT_TOKENS,
|
|
27787
|
+
type=int,
|
|
27788
|
+
help=f"Max output tokens per agent turn (default: {AGENT_MAX_OUTPUT_TOKENS}). Also configurable via config file key 'max_output_tokens'.",
|
|
27789
|
+
)
|
|
26643
27790
|
parser.add_argument(
|
|
26644
27791
|
"--max_user",
|
|
26645
27792
|
default=None,
|
|
@@ -26858,6 +28005,7 @@ def main():
|
|
|
26858
28005
|
or ""
|
|
26859
28006
|
).strip()
|
|
26860
28007
|
resolved_execution_mode = normalize_execution_mode(raw_execution_mode, default=EXECUTION_MODE_SYNC)
|
|
28008
|
+
resolved_max_output_tokens = max(256, int(getattr(args, "max_output_tokens", AGENT_MAX_OUTPUT_TOKENS) or AGENT_MAX_OUTPUT_TOKENS))
|
|
26861
28009
|
if raw_execution_mode:
|
|
26862
28010
|
normalized_raw = str(raw_execution_mode).strip().lower()
|
|
26863
28011
|
if normalized_raw != resolved_execution_mode:
|
|
@@ -26904,6 +28052,7 @@ def main():
|
|
|
26904
28052
|
resolved_arbiter_max_tokens,
|
|
26905
28053
|
resolved_arbiter_temperature,
|
|
26906
28054
|
resolved_execution_mode,
|
|
28055
|
+
resolved_max_output_tokens,
|
|
26907
28056
|
resolved_max_user,
|
|
26908
28057
|
resolved_max_user_sessions,
|
|
26909
28058
|
)
|