clouds-coder 2026.3.16__tar.gz → 2026.3.20__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.16 → clouds_coder-2026.3.20}/Clouds_Coder.py +2389 -72
- {clouds_coder-2026.3.16/clouds_coder.egg-info → clouds_coder-2026.3.20}/PKG-INFO +145 -35
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/README.md +144 -34
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20/clouds_coder.egg-info}/PKG-INFO +145 -35
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/pyproject.toml +1 -1
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/LICENSE +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/setup.cfg +0 -0
- {clouds_coder-2026.3.16 → clouds_coder-2026.3.20}/tests/test_smoke.py +0 -0
|
@@ -44,7 +44,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
|
|
|
44
44
|
CODES_ROOT = WORKDIR / "Codes"
|
|
45
45
|
LLM_CONFIG_PATH = WORKDIR / "LLM.config.json"
|
|
46
46
|
MAX_TOOL_OUTPUT = 50_000
|
|
47
|
-
TOKEN_THRESHOLD =
|
|
47
|
+
TOKEN_THRESHOLD = 1_000_000
|
|
48
48
|
IDLE_TIMEOUT = 60
|
|
49
49
|
POLL_INTERVAL = 5
|
|
50
50
|
SSE_HEARTBEAT_SECONDS = 15
|
|
@@ -75,6 +75,8 @@ AGENT_MAX_OUTPUT_TOKENS = 16384
|
|
|
75
75
|
OLLAMA_THINKING_TOOL_BUFFER = 4096
|
|
76
76
|
WATCHDOG_INTENT_NO_TOOL_THRESHOLD = 2
|
|
77
77
|
WATCHDOG_REPEAT_NO_TOOL_THRESHOLD = 2
|
|
78
|
+
WATCHDOG_INTENT_NO_TOOL_THRESHOLD_SINGLE = 4
|
|
79
|
+
WATCHDOG_REPEAT_NO_TOOL_THRESHOLD_SINGLE = 4
|
|
78
80
|
WATCHDOG_STATE_STALL_THRESHOLD = 6
|
|
79
81
|
WATCHDOG_CONTEXT_STALL_THRESHOLD = 2
|
|
80
82
|
WATCHDOG_REPEAT_SIMILARITY_THRESHOLD = 0.85
|
|
@@ -91,7 +93,30 @@ TRUNCATION_CONTINUATION_ECHO_CHARS = 12000
|
|
|
91
93
|
TRUNCATION_OVERLAP_SCAN_CHARS = 420
|
|
92
94
|
TRUNCATION_PAIR_SCAN_CHARS = 120_000
|
|
93
95
|
TRUNCATION_LIVE_BUFFER_MAX_CHARS = 32000
|
|
94
|
-
MIN_CONTEXT_TOKEN_LIMIT =
|
|
96
|
+
MIN_CONTEXT_TOKEN_LIMIT = 4_000
|
|
97
|
+
# Tiered compression thresholds (fraction of ctx_left / limit)
|
|
98
|
+
COMPACT_TIER1_PCT = 0.40 # >40% left: normal; 20-40%: tier 1 light
|
|
99
|
+
COMPACT_TIER2_PCT = 0.20 # 10-20%: tier 2 medium
|
|
100
|
+
COMPACT_TIER3_PCT = 0.10 # <10%: tier 3 heavy
|
|
101
|
+
# Absolute minimums — prevent percentage instability at low ctx_left
|
|
102
|
+
COMPACT_TIER1_ABS = 3000
|
|
103
|
+
COMPACT_TIER2_ABS = 1500
|
|
104
|
+
# File buffer
|
|
105
|
+
FILE_BUFFER_CONTENT_THRESHOLD = 2000 # chars: content larger than this gets offloaded
|
|
106
|
+
FILE_BUFFER_MAX_FILES = 500
|
|
107
|
+
# Agent context limits per tier (messages count)
|
|
108
|
+
AGENT_MSG_LIMIT_TIER0 = 800
|
|
109
|
+
AGENT_MSG_LIMIT_TIER1 = 400
|
|
110
|
+
AGENT_MSG_LIMIT_TIER2 = 200
|
|
111
|
+
AGENT_MSG_LIMIT_TIER3 = 80
|
|
112
|
+
AGENT_CTX_LIMIT_TIER0 = 400
|
|
113
|
+
AGENT_CTX_LIMIT_TIER1 = 200
|
|
114
|
+
AGENT_CTX_LIMIT_TIER2 = 100
|
|
115
|
+
AGENT_CTX_LIMIT_TIER3 = 40
|
|
116
|
+
MANAGER_CTX_LIMIT_TIER0 = 400
|
|
117
|
+
MANAGER_CTX_LIMIT_TIER1 = 200
|
|
118
|
+
MANAGER_CTX_LIMIT_TIER2 = 100
|
|
119
|
+
MANAGER_CTX_LIMIT_TIER3 = 40
|
|
95
120
|
MAX_CONTEXT_ARCHIVE_SEGMENTS = 1200
|
|
96
121
|
MODEL_OUTPUT_RETRY_TIMES = 3
|
|
97
122
|
ARBITER_TRIGGER_MIN_CONTENT_CHARS = 50
|
|
@@ -164,21 +189,24 @@ EXECUTION_MODE_CHOICES = (
|
|
|
164
189
|
EXECUTION_MODE_SYNC,
|
|
165
190
|
)
|
|
166
191
|
AGENT_ROLES = ("explorer", "developer", "reviewer")
|
|
167
|
-
AGENT_BUBBLE_ROLES = AGENT_ROLES + ("manager",)
|
|
192
|
+
AGENT_BUBBLE_ROLES = AGENT_ROLES + ("manager", "planner")
|
|
168
193
|
AGENT_ROLE_LABELS = {
|
|
169
194
|
"explorer": "Explorer",
|
|
170
195
|
"developer": "Developer",
|
|
171
196
|
"reviewer": "Reviewer",
|
|
172
197
|
"manager": "Manager",
|
|
198
|
+
"planner": "Planner",
|
|
173
199
|
}
|
|
174
200
|
AGENT_ROLE_BUBBLE_COLORS = {
|
|
175
201
|
"explorer": "#ff5fa2",
|
|
176
202
|
"developer": "#22b455",
|
|
177
203
|
"reviewer": "#dca61b",
|
|
178
204
|
"manager": "#7c3aed",
|
|
205
|
+
"planner": "#e8533f",
|
|
179
206
|
}
|
|
180
207
|
BLACKBOARD_STATUSES = (
|
|
181
208
|
"INITIALIZING",
|
|
209
|
+
"PLANNING",
|
|
182
210
|
"RESEARCHING",
|
|
183
211
|
"CODING",
|
|
184
212
|
"TESTING",
|
|
@@ -252,6 +280,91 @@ SKILL_PROMPT_MAX_ITEMS = 40
|
|
|
252
280
|
SKILL_PROMPT_MAX_CHARS = 2600
|
|
253
281
|
SKILL_RUNTIME_CACHE_MAX_ENTRIES = 48
|
|
254
282
|
SKILL_RUNTIME_CACHE_MAX_BYTES = 2_000_000
|
|
283
|
+
PLAN_MODE_ENABLED_LEVELS = {3, 4, 5}
|
|
284
|
+
PLAN_MODE_FORCED_LEVELS = {4, 5}
|
|
285
|
+
PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
|
|
286
|
+
# Task phase definitions for stage-aware delegation
|
|
287
|
+
TASK_PHASES = ("research", "design", "implement", "test", "review", "deploy")
|
|
288
|
+
TASK_PHASE_ROUTING = {
|
|
289
|
+
"research": "explorer",
|
|
290
|
+
"design": "explorer",
|
|
291
|
+
"implement": "developer",
|
|
292
|
+
"test": "developer",
|
|
293
|
+
"review": "reviewer",
|
|
294
|
+
"deploy": "developer",
|
|
295
|
+
}
|
|
296
|
+
# Complexity keywords for semantic detection
|
|
297
|
+
COMPLEXITY_KEYWORDS = (
|
|
298
|
+
"简单", "复杂", "难", "容易", "快速", "详细", "深入",
|
|
299
|
+
"l1", "l2", "l3", "l4", "l5",
|
|
300
|
+
"simple", "complex", "easy", "hard", "difficult",
|
|
301
|
+
"thorough", "quick", "fast", "lightweight", "heavy",
|
|
302
|
+
)
|
|
303
|
+
PLAN_MODE_EXPLORER_MAX_ROUNDS = 8
|
|
304
|
+
# Reviewer debug mode
|
|
305
|
+
REVIEWER_DEBUG_MODE_MAX_ROUNDS = 6
|
|
306
|
+
REVIEWER_DEBUG_TOOL_ALLOWLIST = {
|
|
307
|
+
"bash", "read_file", "write_file", "edit_file",
|
|
308
|
+
"read_from_blackboard", "write_to_blackboard",
|
|
309
|
+
"finish_task", "finish_current_task",
|
|
310
|
+
}
|
|
311
|
+
EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before forced switch
|
|
312
|
+
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 4096
|
|
313
|
+
PLAN_MODE_MAX_OPTIONS = 3
|
|
314
|
+
PLAN_MODE_RESEARCH_TOOL_ALLOWLIST = {
|
|
315
|
+
"bash", "read_file", "context_recall", "task_get", "task_list",
|
|
316
|
+
"check_background", "read_from_blackboard", "write_to_blackboard",
|
|
317
|
+
"list_skills", "load_skill", "compress",
|
|
318
|
+
}
|
|
319
|
+
FAILURE_LEDGER_MAX_FIXES = 20
|
|
320
|
+
FAILURE_LEDGER_MAX_COMPILE_ERRORS = 15
|
|
321
|
+
FAILURE_LEDGER_MAX_DELEGATIONS = 10
|
|
322
|
+
FAILURE_LEDGER_MAX_STALLS = 8
|
|
323
|
+
FAILURE_LEDGER_MAX_TOOL_FPS = 30
|
|
324
|
+
FAILURE_LEDGER_MAX_ERRORS = 25
|
|
325
|
+
# Error category definitions: (name, {cmd_keywords, output_patterns, success_negatives, label})
|
|
326
|
+
# Priority order matters — first match wins. test before runtime because pytest cmd contains 'python'.
|
|
327
|
+
ERROR_CATEGORY_DEFS: list[tuple[str, dict]] = [
|
|
328
|
+
("test", {
|
|
329
|
+
"cmd_keywords": ["pytest", "unittest", "nose", "jest", "mocha", "vitest", "cargo test", "go test", "phpunit", "rspec", "mix test"],
|
|
330
|
+
"output_patterns": ["FAILED", "ERRORS", "AssertionError", "assert ", "expected ", "failures=", "test failed", "FAIL:"],
|
|
331
|
+
"success_negatives": ["failed", "error", "FAILED"],
|
|
332
|
+
"label": "测试错误",
|
|
333
|
+
}),
|
|
334
|
+
("lint", {
|
|
335
|
+
"cmd_keywords": ["mypy", "pylint", "flake8", "ruff", "eslint", "tsc ", "typescript", "clippy", "golint", "rubocop", "shellcheck"],
|
|
336
|
+
"output_patterns": ["error:", "warning:", "violation", "rule ", "type error", "incompatible type", "unused variable"],
|
|
337
|
+
"success_negatives": ["error:", "violation", "type error"],
|
|
338
|
+
"label": "Lint/类型错误",
|
|
339
|
+
}),
|
|
340
|
+
("compilation", {
|
|
341
|
+
"cmd_keywords": ["make", "gcc", "gfortran", "g++", "clang", "cmake", "cargo build", "javac", "rustc", "msbuild", "dotnet build", "go build"],
|
|
342
|
+
"output_patterns": ["error:", "fatal error", "syntax error", "compile error", "undefined reference", "linker error"],
|
|
343
|
+
"success_negatives": ["error:", "fatal error", "syntax error"],
|
|
344
|
+
"label": "编译错误",
|
|
345
|
+
}),
|
|
346
|
+
("build_package", {
|
|
347
|
+
"cmd_keywords": ["npm run build", "yarn build", "webpack", "vite build", "pip install", "poetry install", "bundle install", "gradle", "mvn ", "maven"],
|
|
348
|
+
"output_patterns": ["error:", "ERR!", "build failed", "ModuleNotFoundError", "cannot find module", "dependency", "resolution failed"],
|
|
349
|
+
"success_negatives": ["error:", "ERR!", "build failed"],
|
|
350
|
+
"label": "构建/包错误",
|
|
351
|
+
}),
|
|
352
|
+
("deploy_infra", {
|
|
353
|
+
"cmd_keywords": ["docker", "kubectl", "terraform", "ansible", "cdk ", "pulumi", "serverless", "sam deploy", "helm"],
|
|
354
|
+
"output_patterns": ["error:", "failed", "denied", "timeout", "unreachable", "ImagePullBackOff", "CrashLoopBackOff"],
|
|
355
|
+
"success_negatives": ["error:", "failed", "denied"],
|
|
356
|
+
"label": "部署/基础设施错误",
|
|
357
|
+
}),
|
|
358
|
+
("runtime", {
|
|
359
|
+
"cmd_keywords": ["python", "node ", "ruby ", "java ", "go run", "cargo run", "php ", "perl ", "bash ", "sh ", "./"],
|
|
360
|
+
"output_patterns": ["Traceback", "Exception", "Error:", "panic:", "segfault", "SIGSEGV", "core dumped", "stack trace", "at Object.", "RuntimeError"],
|
|
361
|
+
"success_negatives": ["Traceback", "Exception", "Error:", "panic:", "segfault"],
|
|
362
|
+
"label": "运行时错误",
|
|
363
|
+
}),
|
|
364
|
+
]
|
|
365
|
+
CHECKPOINT_MAX_COUNT = 3
|
|
366
|
+
CHECKPOINT_INTERVAL_ROUNDS = 6
|
|
367
|
+
PERSISTED_ROUTES_MAX = 8
|
|
255
368
|
HTML_FRONTEND_REQUEST_KEYWORDS = (
|
|
256
369
|
"html",
|
|
257
370
|
"web page",
|
|
@@ -2354,7 +2467,7 @@ def try_read_text(path: Path, max_bytes: int = 400_000) -> str | None:
|
|
|
2354
2467
|
except Exception:
|
|
2355
2468
|
return None
|
|
2356
2469
|
|
|
2357
|
-
def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int = 400) -> str:
|
|
2470
|
+
def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int = 400) -> tuple[str, int, int]:
|
|
2358
2471
|
old_lines = old_text.splitlines()
|
|
2359
2472
|
new_lines = new_text.splitlines()
|
|
2360
2473
|
diff = list(
|
|
@@ -2366,9 +2479,12 @@ def make_unified_diff(path: str, old_text: str, new_text: str, max_lines: int =
|
|
|
2366
2479
|
lineterm="",
|
|
2367
2480
|
)
|
|
2368
2481
|
)
|
|
2369
|
-
if
|
|
2482
|
+
added = sum(1 for ln in diff if ln.startswith("+") and not ln.startswith("+++"))
|
|
2483
|
+
deleted = sum(1 for ln in diff if ln.startswith("-") and not ln.startswith("---"))
|
|
2484
|
+
if max_lines and len(diff) > max_lines:
|
|
2370
2485
|
diff = diff[:max_lines] + [f"... diff truncated, total lines={len(diff)}"]
|
|
2371
|
-
|
|
2486
|
+
text = "\n".join(diff) if diff else f"@@ no textual diff for {path}"
|
|
2487
|
+
return text, added, deleted
|
|
2372
2488
|
|
|
2373
2489
|
def _skip_row(text: str) -> dict:
|
|
2374
2490
|
msg = str(text or "").strip() or "⋮"
|
|
@@ -7226,6 +7342,7 @@ class SessionState:
|
|
|
7226
7342
|
self.multimodal_capability_cache: dict[str, dict] = {}
|
|
7227
7343
|
self.failed_selections: list[str] = []
|
|
7228
7344
|
self.todo = TodoManager()
|
|
7345
|
+
self.single_advance_prompt_enhance = False
|
|
7229
7346
|
self.skills = SkillStore(skills_root)
|
|
7230
7347
|
self.skill_load_cache: dict[str, dict] = {}
|
|
7231
7348
|
self.skills_last_refresh_ts = 0.0
|
|
@@ -7270,6 +7387,11 @@ class SessionState:
|
|
|
7270
7387
|
self.runtime_reclassify_goal = ""
|
|
7271
7388
|
self.runtime_reclassify_required = False
|
|
7272
7389
|
self.runtime_goal_reset_pending = False
|
|
7390
|
+
self.runtime_plan_mode_needed = False
|
|
7391
|
+
self.runtime_plan_approved = False
|
|
7392
|
+
self.runtime_plan_proposal = {}
|
|
7393
|
+
self.runtime_plan_choice = ""
|
|
7394
|
+
self.plan_mode_user_preference = "auto" # "auto"|"on"|"off"
|
|
7273
7395
|
self.rounds_without_todo = 0
|
|
7274
7396
|
self.last_todo_reminder_ts = 0.0
|
|
7275
7397
|
self.todo_reminder_count = 0
|
|
@@ -7277,6 +7399,12 @@ class SessionState:
|
|
|
7277
7399
|
self.todo_last_issue = ""
|
|
7278
7400
|
self.last_toolcall_overflow_hint_ts = 0.0
|
|
7279
7401
|
self.last_reviewer_gate_warn_ts = 0.0
|
|
7402
|
+
self.reviewer_debug_mode = False
|
|
7403
|
+
self.reviewer_debug_rounds = 0
|
|
7404
|
+
self.reviewer_debug_context = ""
|
|
7405
|
+
self.explorer_consecutive_delegations = 0
|
|
7406
|
+
self.last_explorer_instruction_hash = ""
|
|
7407
|
+
self._pending_media_inputs: list[dict] = []
|
|
7280
7408
|
self.tool_retry_counts: dict[str, int] = {}
|
|
7281
7409
|
self.last_auto_title_ts = 0.0
|
|
7282
7410
|
self.live_thinking_text = ""
|
|
@@ -7331,6 +7459,9 @@ class SessionState:
|
|
|
7331
7459
|
self.context_archives: list[dict] = []
|
|
7332
7460
|
self.last_compact_reason = ""
|
|
7333
7461
|
self.last_compact_ts = 0.0
|
|
7462
|
+
self.file_buffer_dir = self.root / "file_buffer"
|
|
7463
|
+
self.file_buffer_dir.mkdir(parents=True, exist_ok=True)
|
|
7464
|
+
self.file_buffer_index: dict[str, dict] = {} # ref_id -> {path, chars, summary}
|
|
7334
7465
|
self.created_at = now_ts()
|
|
7335
7466
|
self.updated_at = now_ts()
|
|
7336
7467
|
self.shutdown_requests: dict[str, dict] = {}
|
|
@@ -7471,6 +7602,21 @@ class SessionState:
|
|
|
7471
7602
|
used_rel=used_rel,
|
|
7472
7603
|
)
|
|
7473
7604
|
items.extend(extra)
|
|
7605
|
+
# Merge pending media inputs from read_file (multimodal file reads)
|
|
7606
|
+
with self.lock:
|
|
7607
|
+
pending = list(getattr(self, "_pending_media_inputs", []))
|
|
7608
|
+
self._pending_media_inputs = []
|
|
7609
|
+
for media in pending:
|
|
7610
|
+
kind = str(media.get("type", "") or "").strip().lower()
|
|
7611
|
+
if kind not in kind_allow:
|
|
7612
|
+
continue
|
|
7613
|
+
rel = str(media.get("workspace_path", "") or "")
|
|
7614
|
+
if rel in used_rel:
|
|
7615
|
+
continue
|
|
7616
|
+
if len(items) >= max_items:
|
|
7617
|
+
break
|
|
7618
|
+
items.append(media)
|
|
7619
|
+
used_rel.add(rel)
|
|
7474
7620
|
return items
|
|
7475
7621
|
|
|
7476
7622
|
def _latest_user_message_ts(self) -> float:
|
|
@@ -8184,8 +8330,25 @@ class SessionState:
|
|
|
8184
8330
|
}
|
|
8185
8331
|
)
|
|
8186
8332
|
self.context_archives = clean_archives[-MAX_CONTEXT_ARCHIVE_SEGMENTS:]
|
|
8333
|
+
# Restore file_buffer_index
|
|
8334
|
+
raw_fbi = raw.get("file_buffer_index", {})
|
|
8335
|
+
if isinstance(raw_fbi, dict):
|
|
8336
|
+
clean_fbi: dict[str, dict] = {}
|
|
8337
|
+
for ref_id, entry in raw_fbi.items():
|
|
8338
|
+
if isinstance(entry, dict) and entry.get("path"):
|
|
8339
|
+
clean_fbi[str(ref_id)] = {
|
|
8340
|
+
"path": str(entry.get("path", "")),
|
|
8341
|
+
"chars": int(entry.get("chars", 0) or 0),
|
|
8342
|
+
"summary": trim(str(entry.get("summary", "") or ""), 200),
|
|
8343
|
+
}
|
|
8344
|
+
self.file_buffer_index = clean_fbi
|
|
8187
8345
|
self.last_compact_reason = str(raw.get("last_compact_reason", self.last_compact_reason) or "")
|
|
8188
8346
|
self.last_compact_ts = float(raw.get("last_compact_ts", self.last_compact_ts) or 0.0)
|
|
8347
|
+
pref = str(raw.get("plan_mode_user_preference", "auto") or "auto").lower()
|
|
8348
|
+
self.plan_mode_user_preference = pref if pref in PLAN_MODE_USER_CHOICES else "auto"
|
|
8349
|
+
self.reviewer_debug_mode = bool(raw.get("reviewer_debug_mode", False))
|
|
8350
|
+
self.reviewer_debug_rounds = int(raw.get("reviewer_debug_rounds", 0) or 0)
|
|
8351
|
+
self.reviewer_debug_context = trim(str(raw.get("reviewer_debug_context", "") or ""), 1000)
|
|
8189
8352
|
pending_inputs = raw.get("pending_user_inputs", [])
|
|
8190
8353
|
if isinstance(pending_inputs, list):
|
|
8191
8354
|
clean_pending: list[dict] = []
|
|
@@ -8280,6 +8443,12 @@ class SessionState:
|
|
|
8280
8443
|
self.runtime_goal_reset_pending = bool(
|
|
8281
8444
|
raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
|
|
8282
8445
|
)
|
|
8446
|
+
# Restore plan mode state
|
|
8447
|
+
self.runtime_plan_mode_needed = bool(raw.get("runtime_plan_mode_needed", False))
|
|
8448
|
+
self.runtime_plan_approved = bool(raw.get("runtime_plan_approved", False))
|
|
8449
|
+
raw_proposal = raw.get("runtime_plan_proposal", {})
|
|
8450
|
+
self.runtime_plan_proposal = dict(raw_proposal) if isinstance(raw_proposal, dict) else {}
|
|
8451
|
+
self.runtime_plan_choice = str(raw.get("runtime_plan_choice", "") or "")
|
|
8283
8452
|
self.active_agent_role = str(raw.get("active_agent_role", self.active_agent_role) or "").strip().lower()
|
|
8284
8453
|
raw_contexts = raw.get("contexts", {})
|
|
8285
8454
|
if isinstance(raw_contexts, dict):
|
|
@@ -8394,8 +8563,13 @@ class SessionState:
|
|
|
8394
8563
|
"last_truncation_ts": self.last_truncation_ts,
|
|
8395
8564
|
"truncation_rescue_task_ids": self.truncation_rescue_task_ids[:12],
|
|
8396
8565
|
"context_archives": self.context_archives[-MAX_CONTEXT_ARCHIVE_SEGMENTS:],
|
|
8566
|
+
"file_buffer_index": dict(self.file_buffer_index) if self.file_buffer_index else {},
|
|
8397
8567
|
"last_compact_reason": self.last_compact_reason,
|
|
8398
8568
|
"last_compact_ts": self.last_compact_ts,
|
|
8569
|
+
"plan_mode_user_preference": str(self.plan_mode_user_preference or "auto"),
|
|
8570
|
+
"reviewer_debug_mode": bool(self.reviewer_debug_mode),
|
|
8571
|
+
"reviewer_debug_rounds": int(self.reviewer_debug_rounds or 0),
|
|
8572
|
+
"reviewer_debug_context": trim(str(self.reviewer_debug_context or ""), 1000),
|
|
8399
8573
|
"pending_user_inputs": self.pending_user_inputs[-40:],
|
|
8400
8574
|
"run_generation": int(self.run_generation),
|
|
8401
8575
|
"agent_round_index": int(self.agent_round_index),
|
|
@@ -8427,6 +8601,10 @@ class SessionState:
|
|
|
8427
8601
|
"runtime_reclassify_goal": trim(str(self.runtime_reclassify_goal or ""), 4000),
|
|
8428
8602
|
"runtime_reclassify_required": bool(self.runtime_reclassify_required),
|
|
8429
8603
|
"runtime_goal_reset_pending": bool(self.runtime_goal_reset_pending),
|
|
8604
|
+
"runtime_plan_mode_needed": bool(self.runtime_plan_mode_needed),
|
|
8605
|
+
"runtime_plan_approved": bool(self.runtime_plan_approved),
|
|
8606
|
+
"runtime_plan_proposal": dict(self.runtime_plan_proposal) if self.runtime_plan_proposal else {},
|
|
8607
|
+
"runtime_plan_choice": str(self.runtime_plan_choice or ""),
|
|
8430
8608
|
"active_agent_role": str(self.active_agent_role or ""),
|
|
8431
8609
|
"contexts": {role: list(self.contexts.get(role, []))[-400:] for role in AGENT_ROLES},
|
|
8432
8610
|
"manager_context": list(self.manager_context)[-400:],
|
|
@@ -8574,6 +8752,7 @@ class SessionState:
|
|
|
8574
8752
|
self.runtime_reclassify_goal = ""
|
|
8575
8753
|
self.runtime_reclassify_required = False
|
|
8576
8754
|
self.runtime_goal_reset_pending = False
|
|
8755
|
+
self.runtime_plan_mode_needed = False
|
|
8577
8756
|
return removed_hints
|
|
8578
8757
|
|
|
8579
8758
|
def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
|
|
@@ -8900,6 +9079,23 @@ class SessionState:
|
|
|
8900
9079
|
budget = int(self.runtime_round_budget or 0)
|
|
8901
9080
|
html_block = f"{html_hint}\n\n" if html_hint else ""
|
|
8902
9081
|
research_block = f"{research_hint}\n\n" if research_hint else ""
|
|
9082
|
+
_is_single_no_enhance = (
|
|
9083
|
+
runtime_mode == EXECUTION_MODE_SINGLE
|
|
9084
|
+
and not self.single_advance_prompt_enhance
|
|
9085
|
+
)
|
|
9086
|
+
skill_hint = (
|
|
9087
|
+
"Use load_skill for workspace-paths and tool-best-practices if needed. "
|
|
9088
|
+
if _is_single_no_enhance
|
|
9089
|
+
else (
|
|
9090
|
+
"Use load_skill for guidance on specific topics "
|
|
9091
|
+
"(workspace-paths, task-management, tool-best-practices, context-management). "
|
|
9092
|
+
"If execution stalls, load_skill('execution-degradation-recovery'). "
|
|
9093
|
+
)
|
|
9094
|
+
)
|
|
9095
|
+
plan_steps_block = ""
|
|
9096
|
+
plan_ctx = self._plan_steps_context_for_manager()
|
|
9097
|
+
if plan_ctx:
|
|
9098
|
+
plan_steps_block = f"{plan_ctx}\n"
|
|
8903
9099
|
return (
|
|
8904
9100
|
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
8905
9101
|
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
@@ -8908,9 +9104,8 @@ class SessionState:
|
|
|
8908
9104
|
f"{_detect_os_shell_instruction()} "
|
|
8909
9105
|
"Use tools to inspect, edit, and execute. "
|
|
8910
9106
|
"Call finish_current_task when done. "
|
|
8911
|
-
"
|
|
8912
|
-
"
|
|
8913
|
-
"If execution stalls, load_skill('execution-degradation-recovery'). "
|
|
9107
|
+
f"{skill_hint}"
|
|
9108
|
+
f"{plan_steps_block}"
|
|
8914
9109
|
f"{html_block}"
|
|
8915
9110
|
f"{research_block}"
|
|
8916
9111
|
f"{model_language_instruction(self.ui_language)}\n\n"
|
|
@@ -8919,7 +9114,28 @@ class SessionState:
|
|
|
8919
9114
|
)
|
|
8920
9115
|
|
|
8921
9116
|
def _estimate_tokens(self) -> int:
|
|
8922
|
-
|
|
9117
|
+
# Core: messages (global or agent context depending on mode)
|
|
9118
|
+
if self._is_multi_agent_mode():
|
|
9119
|
+
# In multi-agent mode, the actual context sent to the model is per-role
|
|
9120
|
+
# agent_messages filtered by active role. Use the largest agent context
|
|
9121
|
+
# as the representative cost, since that's the bottleneck.
|
|
9122
|
+
agent_max = 0
|
|
9123
|
+
for role in (self.runtime_participants or AGENT_ROLES):
|
|
9124
|
+
ctx = self._agent_context(role)
|
|
9125
|
+
if ctx:
|
|
9126
|
+
cost = len(json_dumps(ctx)) // 4
|
|
9127
|
+
if cost > agent_max:
|
|
9128
|
+
agent_max = cost
|
|
9129
|
+
msg_tokens = max(agent_max, len(json_dumps(self.messages)) // 4)
|
|
9130
|
+
else:
|
|
9131
|
+
msg_tokens = len(json_dumps(self.messages)) // 4
|
|
9132
|
+
# Overhead: system prompt + tools (always present in API calls)
|
|
9133
|
+
try:
|
|
9134
|
+
sys_tokens = len(self._system_prompt()) // 4
|
|
9135
|
+
except Exception:
|
|
9136
|
+
sys_tokens = 300
|
|
9137
|
+
tools_tokens = len(json_dumps(TOOLS)) // 4 if TOOLS else 0
|
|
9138
|
+
return msg_tokens + sys_tokens + tools_tokens
|
|
8923
9139
|
|
|
8924
9140
|
def _context_budget_metrics(self, token_estimate: int | None = None) -> dict:
|
|
8925
9141
|
limit = max(1, int(self.context_token_upper_bound or 0))
|
|
@@ -8935,8 +9151,143 @@ class SessionState:
|
|
|
8935
9151
|
"used_percent": used_pct,
|
|
8936
9152
|
}
|
|
8937
9153
|
|
|
9154
|
+
def _context_compression_tier(self, metrics: dict | None = None) -> int:
|
|
9155
|
+
"""Return compression tier 0-3 based on context budget.
|
|
9156
|
+
Tier 0: >40% left (normal)
|
|
9157
|
+
Tier 1: 20-40% left (light — aggressive microcompact, trim outputs)
|
|
9158
|
+
Tier 2: 10-20% left (medium — compact agent contexts, file buffer)
|
|
9159
|
+
Tier 3: <10% left (heavy — deep compact everything)
|
|
9160
|
+
"""
|
|
9161
|
+
m = metrics or self._context_budget_metrics()
|
|
9162
|
+
left = int(m.get("left", 0))
|
|
9163
|
+
left_pct = float(m.get("left_percent", 100.0)) / 100.0
|
|
9164
|
+
if left < COMPACT_TIER2_ABS:
|
|
9165
|
+
return 3
|
|
9166
|
+
if left < COMPACT_TIER1_ABS:
|
|
9167
|
+
return max(2, 3 if left_pct < COMPACT_TIER3_PCT else 2)
|
|
9168
|
+
if left_pct < COMPACT_TIER3_PCT:
|
|
9169
|
+
return 3
|
|
9170
|
+
if left_pct < COMPACT_TIER2_PCT:
|
|
9171
|
+
return 2
|
|
9172
|
+
if left_pct < COMPACT_TIER1_PCT:
|
|
9173
|
+
return 1
|
|
9174
|
+
return 0
|
|
9175
|
+
|
|
9176
|
+
def _tier_agent_context_limits(self, tier: int) -> dict:
|
|
9177
|
+
"""Return message count limits for agent_messages, contexts, manager_context by tier."""
|
|
9178
|
+
t = min(max(tier, 0), 3)
|
|
9179
|
+
return {
|
|
9180
|
+
"agent_messages": [AGENT_MSG_LIMIT_TIER0, AGENT_MSG_LIMIT_TIER1, AGENT_MSG_LIMIT_TIER2, AGENT_MSG_LIMIT_TIER3][t],
|
|
9181
|
+
"context_per_role": [AGENT_CTX_LIMIT_TIER0, AGENT_CTX_LIMIT_TIER1, AGENT_CTX_LIMIT_TIER2, AGENT_CTX_LIMIT_TIER3][t],
|
|
9182
|
+
"manager_context": [MANAGER_CTX_LIMIT_TIER0, MANAGER_CTX_LIMIT_TIER1, MANAGER_CTX_LIMIT_TIER2, MANAGER_CTX_LIMIT_TIER3][t],
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
def _compact_agent_contexts(self, tier: int):
|
|
9186
|
+
"""Compress agent_messages, manager_context, and per-role contexts based on tier."""
|
|
9187
|
+
limits = self._tier_agent_context_limits(tier)
|
|
9188
|
+
am_limit = limits["agent_messages"]
|
|
9189
|
+
ctx_limit = limits["context_per_role"]
|
|
9190
|
+
mgr_limit = limits["manager_context"]
|
|
9191
|
+
# 1. Microcompact agent messages (tier >= 1)
|
|
9192
|
+
if tier >= 1:
|
|
9193
|
+
keep = max(1, 3 - tier) # tier1=2, tier2=1, tier3=0(but min 1)
|
|
9194
|
+
self._microcompact_agent_messages(self.agent_messages, keep_recent=keep)
|
|
9195
|
+
# 2. Trim agent_messages to limit
|
|
9196
|
+
if len(self.agent_messages) > am_limit:
|
|
9197
|
+
self.agent_messages = self.agent_messages[-am_limit:]
|
|
9198
|
+
# 3. Trim manager_context
|
|
9199
|
+
if len(self.manager_context) > mgr_limit:
|
|
9200
|
+
self.manager_context = self.manager_context[-mgr_limit:]
|
|
9201
|
+
# 4. Trim per-role contexts
|
|
9202
|
+
for role in AGENT_ROLES:
|
|
9203
|
+
ctx = self.contexts.get(role, [])
|
|
9204
|
+
if len(ctx) > ctx_limit:
|
|
9205
|
+
self.contexts[role] = ctx[-ctx_limit:]
|
|
9206
|
+
# 5. Tier 2+: offload large content in agent messages to file buffer
|
|
9207
|
+
if tier >= 2:
|
|
9208
|
+
self._offload_agent_message_content(self.agent_messages)
|
|
9209
|
+
self._offload_agent_message_content(self.manager_context)
|
|
9210
|
+
# 6. Tier 3: aggressive content trimming
|
|
9211
|
+
if tier >= 3:
|
|
9212
|
+
for msg in self.agent_messages:
|
|
9213
|
+
c = msg.get("content", "")
|
|
9214
|
+
if isinstance(c, str) and len(c) > 600:
|
|
9215
|
+
msg["content"] = trim(c, 600)
|
|
9216
|
+
for msg in self.manager_context:
|
|
9217
|
+
c = msg.get("content", "")
|
|
9218
|
+
if isinstance(c, str) and len(c) > 600:
|
|
9219
|
+
msg["content"] = trim(c, 600)
|
|
9220
|
+
# 7. Plan mode context compression — tiered by ctx_left
|
|
9221
|
+
self._compact_plan_context(tier)
|
|
9222
|
+
|
|
9223
|
+
def _compact_plan_context(self, tier: int):
|
|
9224
|
+
"""Compress plan mode context based on tier.
|
|
9225
|
+
Tier 0: keep everything
|
|
9226
|
+
Tier 1: trim plan findings to last 3
|
|
9227
|
+
Tier 2: drop plan findings, keep only plan steps + user choice
|
|
9228
|
+
Tier 3: minimal — only plan steps and chosen option title
|
|
9229
|
+
"""
|
|
9230
|
+
bb = self._ensure_blackboard()
|
|
9231
|
+
plan = bb.get("plan")
|
|
9232
|
+
if not isinstance(plan, dict):
|
|
9233
|
+
return
|
|
9234
|
+
if tier <= 0:
|
|
9235
|
+
return
|
|
9236
|
+
if tier == 1:
|
|
9237
|
+
# Keep last 3 findings
|
|
9238
|
+
findings = plan.get("findings", [])
|
|
9239
|
+
if isinstance(findings, list) and len(findings) > 3:
|
|
9240
|
+
plan["findings"] = findings[-3:]
|
|
9241
|
+
elif tier == 2:
|
|
9242
|
+
# Drop findings entirely, keep steps + proposal summary
|
|
9243
|
+
plan.pop("findings", None)
|
|
9244
|
+
proposal = plan.get("proposal", {})
|
|
9245
|
+
if isinstance(proposal, dict):
|
|
9246
|
+
# Trim option details — keep only chosen option
|
|
9247
|
+
chosen_id = str(plan.get("chosen", "") or "")
|
|
9248
|
+
options = proposal.get("options", [])
|
|
9249
|
+
if isinstance(options, list) and chosen_id:
|
|
9250
|
+
proposal["options"] = [
|
|
9251
|
+
o for o in options
|
|
9252
|
+
if isinstance(o, dict) and o.get("id") == chosen_id
|
|
9253
|
+
]
|
|
9254
|
+
# Trim chosen option — drop pros/cons/risk
|
|
9255
|
+
for o in proposal.get("options", []):
|
|
9256
|
+
o.pop("pros", None)
|
|
9257
|
+
o.pop("cons", None)
|
|
9258
|
+
o.pop("risk", None)
|
|
9259
|
+
elif tier >= 3:
|
|
9260
|
+
# Minimal: only phase, chosen, steps
|
|
9261
|
+
phase = plan.get("phase", "")
|
|
9262
|
+
chosen = plan.get("chosen", "")
|
|
9263
|
+
steps = plan.get("steps", [])
|
|
9264
|
+
bb["plan"] = {"phase": phase, "chosen": chosen, "steps": steps}
|
|
9265
|
+
self.blackboard = bb
|
|
9266
|
+
# Also trim plan proposal in runtime state
|
|
9267
|
+
if tier >= 2 and self.runtime_plan_proposal:
|
|
9268
|
+
chosen_id = str(self.runtime_plan_choice or "")
|
|
9269
|
+
if chosen_id:
|
|
9270
|
+
options = self.runtime_plan_proposal.get("options", [])
|
|
9271
|
+
if isinstance(options, list):
|
|
9272
|
+
self.runtime_plan_proposal["options"] = [
|
|
9273
|
+
o for o in options if isinstance(o, dict) and o.get("id") == chosen_id
|
|
9274
|
+
]
|
|
9275
|
+
if tier >= 3:
|
|
9276
|
+
self.runtime_plan_proposal = {}
|
|
9277
|
+
|
|
8938
9278
|
def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
|
|
8939
|
-
self.
|
|
9279
|
+
metrics = self._context_budget_metrics()
|
|
9280
|
+
tier = self._context_compression_tier(metrics)
|
|
9281
|
+
# Tier 1+: apply progressively aggressive microcompact
|
|
9282
|
+
if tier >= 1:
|
|
9283
|
+
keep = max(1, 3 - tier)
|
|
9284
|
+
self._microcompact(keep_recent=keep)
|
|
9285
|
+
else:
|
|
9286
|
+
self._microcompact()
|
|
9287
|
+
# Tier 2+: compact agent contexts proactively
|
|
9288
|
+
if tier >= 2:
|
|
9289
|
+
self._compact_agent_contexts(tier)
|
|
9290
|
+
# Re-check after tier-based compression
|
|
8940
9291
|
metrics = self._context_budget_metrics()
|
|
8941
9292
|
used = int(metrics.get("used", 0) or 0)
|
|
8942
9293
|
limit = max(1, int(metrics.get("limit", 0) or 0))
|
|
@@ -9740,10 +10091,10 @@ class SessionState:
|
|
|
9740
10091
|
total += len(str(row.get("content", ""))) // 4
|
|
9741
10092
|
return total
|
|
9742
10093
|
|
|
9743
|
-
def _select_compact_tail(self, token_budget: int, min_count: int =
|
|
10094
|
+
def _select_compact_tail(self, token_budget: int, min_count: int = 4, max_count: int = 48) -> list[dict]:
|
|
9744
10095
|
if not self.messages:
|
|
9745
10096
|
return []
|
|
9746
|
-
budget = max(
|
|
10097
|
+
budget = max(1000, int(token_budget))
|
|
9747
10098
|
selected: list[dict] = []
|
|
9748
10099
|
total = 0
|
|
9749
10100
|
for row in reversed(self.messages):
|
|
@@ -9793,6 +10144,60 @@ class SessionState:
|
|
|
9793
10144
|
self.context_archives = self.context_archives[-MAX_CONTEXT_ARCHIVE_SEGMENTS:]
|
|
9794
10145
|
return seg
|
|
9795
10146
|
|
|
10147
|
+
# --- File buffer methods (修改 7) ---
|
|
10148
|
+
|
|
10149
|
+
def _offload_to_file_buffer(self, content: str, label: str = "") -> str:
|
|
10150
|
+
"""Write large content to disk, return compact reference placeholder."""
|
|
10151
|
+
if len(content) < FILE_BUFFER_CONTENT_THRESHOLD:
|
|
10152
|
+
return content
|
|
10153
|
+
ref_id = f"fb_{int(now_ts())}_{uuid.uuid4().hex[:6]}"
|
|
10154
|
+
fp = self.file_buffer_dir / f"{ref_id}.txt"
|
|
10155
|
+
fp.write_text(content, encoding="utf-8")
|
|
10156
|
+
summary = trim(content, 120).replace("\n", " ")
|
|
10157
|
+
self.file_buffer_index[ref_id] = {
|
|
10158
|
+
"path": fp.relative_to(self.root).as_posix(),
|
|
10159
|
+
"chars": len(content),
|
|
10160
|
+
"summary": summary,
|
|
10161
|
+
}
|
|
10162
|
+
# Prune old entries if over limit
|
|
10163
|
+
if len(self.file_buffer_index) > FILE_BUFFER_MAX_FILES:
|
|
10164
|
+
oldest_keys = sorted(self.file_buffer_index.keys())[:len(self.file_buffer_index) - FILE_BUFFER_MAX_FILES]
|
|
10165
|
+
for k in oldest_keys:
|
|
10166
|
+
old_path = self.root / self.file_buffer_index[k].get("path", "")
|
|
10167
|
+
if old_path.exists():
|
|
10168
|
+
try:
|
|
10169
|
+
old_path.unlink()
|
|
10170
|
+
except Exception:
|
|
10171
|
+
pass
|
|
10172
|
+
del self.file_buffer_index[k]
|
|
10173
|
+
return f"[file_buffer:{ref_id} chars={len(content)} summary={summary}]"
|
|
10174
|
+
|
|
10175
|
+
def _load_from_file_buffer(self, ref_id: str) -> str:
|
|
10176
|
+
"""Load content from file buffer by reference ID."""
|
|
10177
|
+
entry = self.file_buffer_index.get(ref_id)
|
|
10178
|
+
if not entry:
|
|
10179
|
+
return f"[file_buffer:{ref_id} not found]"
|
|
10180
|
+
fp = self.root / entry.get("path", "")
|
|
10181
|
+
if not fp.exists():
|
|
10182
|
+
return f"[file_buffer:{ref_id} file missing]"
|
|
10183
|
+
try:
|
|
10184
|
+
return fp.read_text(encoding="utf-8")
|
|
10185
|
+
except Exception as exc:
|
|
10186
|
+
return f"[file_buffer:{ref_id} read error: {exc}]"
|
|
10187
|
+
|
|
10188
|
+
def _offload_agent_message_content(self, messages: list[dict]):
|
|
10189
|
+
"""Scan messages and offload large string content to file buffer."""
|
|
10190
|
+
for msg in messages:
|
|
10191
|
+
content = msg.get("content", "")
|
|
10192
|
+
if isinstance(content, str) and len(content) >= FILE_BUFFER_CONTENT_THRESHOLD:
|
|
10193
|
+
msg["content"] = self._offload_to_file_buffer(content, label=msg.get("role", ""))
|
|
10194
|
+
elif isinstance(content, list):
|
|
10195
|
+
for block in content:
|
|
10196
|
+
if isinstance(block, dict):
|
|
10197
|
+
bc = block.get("content", "")
|
|
10198
|
+
if isinstance(bc, str) and len(bc) >= FILE_BUFFER_CONTENT_THRESHOLD:
|
|
10199
|
+
block["content"] = self._offload_to_file_buffer(bc, label=block.get("type", ""))
|
|
10200
|
+
|
|
9796
10201
|
def _summarize_compact_rows(self, rows: list[dict]) -> str:
|
|
9797
10202
|
if not rows:
|
|
9798
10203
|
return "(no archived rows)"
|
|
@@ -9865,17 +10270,145 @@ class SessionState:
|
|
|
9865
10270
|
lines.append(f"Current focus: {focus}")
|
|
9866
10271
|
lines.append(f"Open tasks ({len(task_open)}):")
|
|
9867
10272
|
lines.extend(task_open[:6] if task_open else ["- none"])
|
|
10273
|
+
# Checkpoint drift detection
|
|
10274
|
+
bb = self._ensure_blackboard()
|
|
10275
|
+
cps = bb.get("checkpoints", [])
|
|
10276
|
+
if isinstance(cps, list) and len(cps) >= 2:
|
|
10277
|
+
latest_cp = cps[-1] if isinstance(cps[-1], dict) else {}
|
|
10278
|
+
prev_cp = cps[-2] if isinstance(cps[-2], dict) else {}
|
|
10279
|
+
if self._checkpoint_detect_drift(latest_cp, prev_cp):
|
|
10280
|
+
cp_round = int(latest_cp.get("round", 0) or 0)
|
|
10281
|
+
lines.append(f"⚠️ 自 round {cp_round} 以来无实质进展,需要换策略。")
|
|
9868
10282
|
return "\n".join(lines)
|
|
9869
10283
|
|
|
10284
|
+
def _failure_aware_brief(self) -> str:
|
|
10285
|
+
bb = self._ensure_blackboard()
|
|
10286
|
+
fl = bb.get("failure_ledger")
|
|
10287
|
+
if not isinstance(fl, dict):
|
|
10288
|
+
return ""
|
|
10289
|
+
lines: list[str] = []
|
|
10290
|
+
# Unresolved errors (unified) — grouped by category
|
|
10291
|
+
all_errors = fl.get("errors", [])
|
|
10292
|
+
unresolved = [e for e in all_errors if isinstance(e, dict) and int(e.get("count", 0) or 0) > 0]
|
|
10293
|
+
# Fallback: if errors is empty but compilation_errors has data, use that
|
|
10294
|
+
if not unresolved:
|
|
10295
|
+
comp_errors = fl.get("compilation_errors", [])
|
|
10296
|
+
unresolved = [e for e in comp_errors if isinstance(e, dict) and int(e.get("count", 0) or 0) > 0]
|
|
10297
|
+
for e in unresolved:
|
|
10298
|
+
if "category" not in e:
|
|
10299
|
+
e["category"] = "compilation"
|
|
10300
|
+
if unresolved:
|
|
10301
|
+
lines.append("## ⚠️ 已知问题(勿重复尝试)")
|
|
10302
|
+
by_cat: dict[str, list] = {}
|
|
10303
|
+
for e in unresolved:
|
|
10304
|
+
cat = str(e.get("category", "unknown") or "unknown")
|
|
10305
|
+
by_cat.setdefault(cat, []).append(e)
|
|
10306
|
+
cat_labels = dict(ERROR_CATEGORY_DEFS)
|
|
10307
|
+
for cat, errs in by_cat.items():
|
|
10308
|
+
label = cat_labels.get(cat, {}).get("label", cat) if cat in cat_labels else cat
|
|
10309
|
+
lines.append(f"{label} (未解决):")
|
|
10310
|
+
for e in errs[:8]:
|
|
10311
|
+
f = str(e.get("file", "") or "")
|
|
10312
|
+
msg = str(e.get("error_msg", "") or "")
|
|
10313
|
+
cnt = int(e.get("count", 1) or 1)
|
|
10314
|
+
lines.append(f"- {f}: {trim(msg, 120)} (出现 {cnt} 次)")
|
|
10315
|
+
# Failed fix attempts
|
|
10316
|
+
fixes = fl.get("attempted_fixes", [])
|
|
10317
|
+
if fixes:
|
|
10318
|
+
lines.append("已尝试修复 (失败):")
|
|
10319
|
+
for fx in fixes[-6:]:
|
|
10320
|
+
if not isinstance(fx, dict):
|
|
10321
|
+
continue
|
|
10322
|
+
fix_desc = str(fx.get("fix", "") or "")
|
|
10323
|
+
result = str(fx.get("result", "") or "")
|
|
10324
|
+
lines.append(f"- {trim(fix_desc, 100)} → {trim(result, 60)}")
|
|
10325
|
+
# Repeated delegations
|
|
10326
|
+
delegations = fl.get("repeated_delegations", [])
|
|
10327
|
+
repeated = [d for d in delegations if isinstance(d, dict) and int(d.get("count", 0) or 0) >= 2]
|
|
10328
|
+
if repeated:
|
|
10329
|
+
lines.append("重复委派 (需换策略):")
|
|
10330
|
+
for d in repeated[:5]:
|
|
10331
|
+
tgt = str(d.get("target", "") or "")
|
|
10332
|
+
cnt = int(d.get("count", 1) or 1)
|
|
10333
|
+
preview = str(d.get("instruction_preview", "") or "")
|
|
10334
|
+
lines.append(f"- {tgt} 收到相同指令 {cnt} 次: {trim(preview, 80)}")
|
|
10335
|
+
# Stall events
|
|
10336
|
+
stalls = fl.get("stall_events", [])
|
|
10337
|
+
if stalls:
|
|
10338
|
+
lines.append(f"停滞事件: {len(stalls)} 次 (最近: {str(stalls[-1].get('reason', '') if isinstance(stalls[-1], dict) else '')})")
|
|
10339
|
+
if not lines:
|
|
10340
|
+
return ""
|
|
10341
|
+
return trim("\n".join(lines), 800)
|
|
10342
|
+
|
|
10343
|
+
def _create_checkpoint_snapshot(self) -> dict:
|
|
10344
|
+
bb = self._ensure_blackboard()
|
|
10345
|
+
fl = bb.get("failure_ledger", {})
|
|
10346
|
+
if not isinstance(fl, dict):
|
|
10347
|
+
fl = {}
|
|
10348
|
+
comp_errors = fl.get("errors", []) or fl.get("compilation_errors", [])
|
|
10349
|
+
active_errors = len([e for e in comp_errors if isinstance(e, dict) and int(e.get("count", 0) or 0) > 0])
|
|
10350
|
+
artifacts = bb.get("code_artifacts", {})
|
|
10351
|
+
artifacts_keys = sorted(artifacts.keys()) if isinstance(artifacts, dict) else []
|
|
10352
|
+
files_hash = hashlib.sha1(str(artifacts_keys).encode("utf-8")).hexdigest()[:12]
|
|
10353
|
+
try:
|
|
10354
|
+
todo_snap = json_dumps(self.todo.snapshot()) if hasattr(self, "todo") else ""
|
|
10355
|
+
except Exception:
|
|
10356
|
+
todo_snap = ""
|
|
10357
|
+
todos_hash = hashlib.sha1(todo_snap.encode("utf-8")).hexdigest()[:12]
|
|
10358
|
+
stall_events = fl.get("stall_events", [])
|
|
10359
|
+
delegations = fl.get("repeated_delegations", [])
|
|
10360
|
+
return {
|
|
10361
|
+
"round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
10362
|
+
"ts": float(now_ts()),
|
|
10363
|
+
"active_errors_count": active_errors,
|
|
10364
|
+
"files_modified_hash": files_hash,
|
|
10365
|
+
"todos_hash": todos_hash,
|
|
10366
|
+
"stall_count": len(stall_events) if isinstance(stall_events, list) else 0,
|
|
10367
|
+
"delegation_count": sum(
|
|
10368
|
+
int(d.get("count", 0) or 0) for d in delegations if isinstance(d, dict)
|
|
10369
|
+
) if isinstance(delegations, list) else 0,
|
|
10370
|
+
}
|
|
10371
|
+
|
|
10372
|
+
def _checkpoint_detect_drift(self, current: dict, previous: dict) -> bool:
|
|
10373
|
+
if not isinstance(current, dict) or not isinstance(previous, dict):
|
|
10374
|
+
return False
|
|
10375
|
+
errors_not_reduced = int(current.get("active_errors_count", 0) or 0) >= int(previous.get("active_errors_count", 0) or 0)
|
|
10376
|
+
files_unchanged = str(current.get("files_modified_hash", "")) == str(previous.get("files_modified_hash", ""))
|
|
10377
|
+
stalls_increased = int(current.get("stall_count", 0) or 0) > int(previous.get("stall_count", 0) or 0)
|
|
10378
|
+
return errors_not_reduced and files_unchanged and stalls_increased
|
|
10379
|
+
|
|
10380
|
+
def _maybe_create_checkpoint(self):
|
|
10381
|
+
bb = self._ensure_blackboard()
|
|
10382
|
+
cps = bb.get("checkpoints", [])
|
|
10383
|
+
if not isinstance(cps, list):
|
|
10384
|
+
cps = []
|
|
10385
|
+
current_round = int(getattr(self, "agent_round_index", 0) or 0)
|
|
10386
|
+
if cps:
|
|
10387
|
+
last_cp = cps[-1] if isinstance(cps[-1], dict) else {}
|
|
10388
|
+
last_round = int(last_cp.get("round", 0) or 0)
|
|
10389
|
+
if current_round - last_round < CHECKPOINT_INTERVAL_ROUNDS:
|
|
10390
|
+
return
|
|
10391
|
+
snapshot = self._create_checkpoint_snapshot()
|
|
10392
|
+
cps.append(snapshot)
|
|
10393
|
+
bb["checkpoints"] = cps[-CHECKPOINT_MAX_COUNT:]
|
|
10394
|
+
self.blackboard = bb
|
|
10395
|
+
|
|
9870
10396
|
def _auto_compact(self, reason: str):
|
|
9871
10397
|
context_before = self._context_budget_metrics()
|
|
10398
|
+
tier = self._context_compression_tier(context_before)
|
|
9872
10399
|
transcript_dir = self.root / "transcripts"
|
|
9873
10400
|
transcript_dir.mkdir(parents=True, exist_ok=True)
|
|
9874
10401
|
transcript_path = transcript_dir / f"transcript_{int(now_ts())}.jsonl"
|
|
9875
10402
|
with transcript_path.open("w", encoding="utf-8") as handle:
|
|
9876
10403
|
for msg in self.messages:
|
|
9877
10404
|
handle.write(json_dumps(msg) + "\n")
|
|
9878
|
-
|
|
10405
|
+
# Adjust tail budget based on tier — lower tier = smaller tail
|
|
10406
|
+
if tier >= 3:
|
|
10407
|
+
tail_budget = max(2000, min(6000, int(self.context_token_upper_bound * 0.20)))
|
|
10408
|
+
elif tier >= 2:
|
|
10409
|
+
tail_budget = max(3000, min(10_000, int(self.context_token_upper_bound * 0.28)))
|
|
10410
|
+
else:
|
|
10411
|
+
tail_budget = max(4500, min(16_000, int(self.context_token_upper_bound * 0.35)))
|
|
9879
10412
|
tail = self._select_compact_tail(tail_budget)
|
|
9880
10413
|
if len(tail) >= len(self.messages):
|
|
9881
10414
|
tail = self._select_compact_tail(max(2200, int(tail_budget * 0.55)), min_count=4, max_count=20)
|
|
@@ -9890,35 +10423,59 @@ class SessionState:
|
|
|
9890
10423
|
if seg_id
|
|
9891
10424
|
else "If details are missing, call context_recall with recent_segments=2."
|
|
9892
10425
|
)
|
|
10426
|
+
# Create checkpoint before compaction
|
|
10427
|
+
self._maybe_create_checkpoint()
|
|
10428
|
+
# Build state handoff for lossless continuity
|
|
10429
|
+
state_handoff = self._build_state_handoff()
|
|
10430
|
+
failure_brief = self._failure_aware_brief()
|
|
10431
|
+
failure_section = f"{failure_brief}\n" if failure_brief else ""
|
|
9893
10432
|
compact_note = (
|
|
9894
10433
|
"<compact-resume>\n"
|
|
9895
10434
|
f"reason: {reason}\n"
|
|
10435
|
+
f"tier: {tier}\n"
|
|
9896
10436
|
f"transcript: {transcript_path}\n"
|
|
9897
10437
|
f"archive_segment: {seg_id or 'none'} messages={seg_msg_count} path={seg_path or '-'}\n"
|
|
10438
|
+
f"{state_handoff}\n"
|
|
9898
10439
|
f"{self._open_work_brief()}\n"
|
|
10440
|
+
f"{failure_section}"
|
|
9899
10441
|
f"summary:\n{summary}\n"
|
|
9900
10442
|
f"{continuation}\n"
|
|
9901
10443
|
"Continue exploring from current pending work; do not restart from scratch.\n"
|
|
9902
10444
|
"</compact-resume>"
|
|
9903
10445
|
)
|
|
9904
10446
|
tail.append({"role": "user", "content": compact_note, "ts": now_ts()})
|
|
9905
|
-
|
|
10447
|
+
# Adjust target tokens based on tier
|
|
10448
|
+
if tier >= 3:
|
|
10449
|
+
target_tokens = max(2000, min(8000, int(self.context_token_upper_bound * 0.35)))
|
|
10450
|
+
elif tier >= 2:
|
|
10451
|
+
target_tokens = max(3000, min(12_000, int(self.context_token_upper_bound * 0.45)))
|
|
10452
|
+
else:
|
|
10453
|
+
target_tokens = max(4000, min(20_000, int(self.context_token_upper_bound * 0.55)))
|
|
9906
10454
|
while len(tail) > 5 and self._estimate_messages_tokens(tail) > target_tokens:
|
|
9907
10455
|
tail.pop(0)
|
|
9908
10456
|
if self._estimate_messages_tokens(tail) > target_tokens:
|
|
9909
10457
|
for msg in tail:
|
|
9910
10458
|
if msg.get("role") == "tool":
|
|
9911
|
-
msg["content"] = trim(msg.get("content", ""), 1800)
|
|
10459
|
+
msg["content"] = trim(msg.get("content", ""), 1800 if tier < 2 else 600)
|
|
9912
10460
|
while len(tail) > 2 and self._estimate_messages_tokens(tail) > target_tokens:
|
|
9913
10461
|
tail.pop(0)
|
|
9914
10462
|
self.messages = tail
|
|
10463
|
+
# Compact agent contexts in sync (critical fix for re-wall prevention)
|
|
10464
|
+
self._compact_agent_contexts(max(tier, 1))
|
|
10465
|
+
# Tier 2+: offload remaining large content in messages to file buffer
|
|
10466
|
+
if tier >= 2:
|
|
10467
|
+
for msg in self.messages:
|
|
10468
|
+
c = msg.get("content", "")
|
|
10469
|
+
if isinstance(c, str) and len(c) >= FILE_BUFFER_CONTENT_THRESHOLD * 2:
|
|
10470
|
+
msg["content"] = self._offload_to_file_buffer(c, label="compact-msg")
|
|
9915
10471
|
self.last_compact_reason = str(reason or "")
|
|
9916
10472
|
self.last_compact_ts = now_ts()
|
|
9917
10473
|
self._emit(
|
|
9918
10474
|
"compact",
|
|
9919
10475
|
{
|
|
9920
|
-
"summary": f"context compacted ({reason}) archives={len(self.context_archives)}",
|
|
10476
|
+
"summary": f"context compacted ({reason}) tier={tier} archives={len(self.context_archives)}",
|
|
9921
10477
|
"reason": str(reason or ""),
|
|
10478
|
+
"tier": tier,
|
|
9922
10479
|
"archive_segment": seg_id,
|
|
9923
10480
|
"archived_messages": seg_msg_count,
|
|
9924
10481
|
"context_limit_before": int(context_before.get("limit", 0)),
|
|
@@ -9928,6 +10485,50 @@ class SessionState:
|
|
|
9928
10485
|
},
|
|
9929
10486
|
)
|
|
9930
10487
|
|
|
10488
|
+
def _build_state_handoff(self) -> str:
|
|
10489
|
+
"""Build a lossless state handoff string for compact-resume note.
|
|
10490
|
+
Captures: goal, progress, current node state, active agent, round info."""
|
|
10491
|
+
bb = self._ensure_blackboard()
|
|
10492
|
+
parts: list[str] = ["<STATE_HANDOFF>"]
|
|
10493
|
+
# 1. Original goal
|
|
10494
|
+
goal = trim(str(bb.get("original_goal", "") or ""), 500)
|
|
10495
|
+
if goal:
|
|
10496
|
+
parts.append(f"GOAL: {goal}")
|
|
10497
|
+
# 2. Current progress / manager judgement
|
|
10498
|
+
judgement = bb.get("manager_judgement", {})
|
|
10499
|
+
if isinstance(judgement, dict):
|
|
10500
|
+
progress = str(judgement.get("progress", "") or "")
|
|
10501
|
+
remaining = judgement.get("remaining_rounds", "")
|
|
10502
|
+
if progress:
|
|
10503
|
+
parts.append(f"PROGRESS: {progress}")
|
|
10504
|
+
if remaining != "":
|
|
10505
|
+
parts.append(f"REMAINING_ROUNDS: {remaining}")
|
|
10506
|
+
# 3. Active agent and phase
|
|
10507
|
+
active_role = str(getattr(self, "runtime_active_role", "") or "")
|
|
10508
|
+
phase = str(getattr(self, "current_phase", "") or "")
|
|
10509
|
+
round_idx = int(getattr(self, "agent_round_index", 0) or 0)
|
|
10510
|
+
parts.append(f"ROUND: {round_idx}, ACTIVE_AGENT: {active_role or 'none'}, PHASE: {phase or 'idle'}")
|
|
10511
|
+
# 4. Task profile
|
|
10512
|
+
tp = bb.get("task_profile", {})
|
|
10513
|
+
if isinstance(tp, dict):
|
|
10514
|
+
task_type = str(tp.get("task_type", "") or "")
|
|
10515
|
+
complexity = str(tp.get("complexity", "") or "")
|
|
10516
|
+
if task_type:
|
|
10517
|
+
parts.append(f"TASK_TYPE: {task_type}, COMPLEXITY: {complexity}")
|
|
10518
|
+
# 5. Last delegate instruction (if multi-agent)
|
|
10519
|
+
last_del = bb.get("last_delegate", {})
|
|
10520
|
+
if isinstance(last_del, dict) and last_del.get("instruction"):
|
|
10521
|
+
parts.append(f"LAST_DELEGATE: {trim(str(last_del.get('instruction', '')), 200)}")
|
|
10522
|
+
# 6. Key blackboard entries (code_artifacts keys, review_feedback)
|
|
10523
|
+
artifacts = bb.get("code_artifacts", {})
|
|
10524
|
+
if isinstance(artifacts, dict) and artifacts:
|
|
10525
|
+
parts.append(f"CODE_ARTIFACTS: {', '.join(sorted(artifacts.keys())[:10])}")
|
|
10526
|
+
review = bb.get("review_feedback", "")
|
|
10527
|
+
if review:
|
|
10528
|
+
parts.append(f"REVIEW_FEEDBACK: {trim(str(review), 200)}")
|
|
10529
|
+
parts.append("</STATE_HANDOFF>")
|
|
10530
|
+
return "\n".join(parts)
|
|
10531
|
+
|
|
9931
10532
|
def _git_status_map(self, cwd: Path) -> dict[str, str]:
|
|
9932
10533
|
try:
|
|
9933
10534
|
check = subprocess.run(
|
|
@@ -11286,7 +11887,7 @@ class SessionState:
|
|
|
11286
11887
|
active_rows.append(trim(content, 140))
|
|
11287
11888
|
if len(active_rows) >= 3:
|
|
11288
11889
|
break
|
|
11289
|
-
|
|
11890
|
+
snapshot = {
|
|
11290
11891
|
"goal": self._latest_user_goal_text(),
|
|
11291
11892
|
"todo_open_count": int(open_count),
|
|
11292
11893
|
"todo_in_progress_count": int(in_progress_count),
|
|
@@ -11295,6 +11896,20 @@ class SessionState:
|
|
|
11295
11896
|
"assistant_thinking": trim(str(thinking_text or "").strip(), 700),
|
|
11296
11897
|
"agent_round_index": int(self.agent_round_index),
|
|
11297
11898
|
}
|
|
11899
|
+
# Inject plan step progress so arbiter knows overall completion state
|
|
11900
|
+
bb = self._ensure_blackboard()
|
|
11901
|
+
plan_todos = bb.get("project_todos", [])
|
|
11902
|
+
plan_steps = [t for t in plan_todos if t.get("category") == "plan_step"]
|
|
11903
|
+
if plan_steps:
|
|
11904
|
+
completed = len([t for t in plan_steps if t.get("status") == "completed"])
|
|
11905
|
+
total = len(plan_steps)
|
|
11906
|
+
snapshot["plan_progress"] = f"{completed}/{total} steps completed"
|
|
11907
|
+
if completed < total:
|
|
11908
|
+
snapshot["plan_warning"] = (
|
|
11909
|
+
f"IMPORTANT: Only {completed}/{total} plan steps are completed. "
|
|
11910
|
+
"Do NOT classify as TASK_COMPLETED unless ALL steps are done."
|
|
11911
|
+
)
|
|
11912
|
+
return snapshot
|
|
11298
11913
|
|
|
11299
11914
|
def _build_arbiter_client(self) -> OllamaClient:
|
|
11300
11915
|
client = OllamaClient(self.ollama.base_url, self.ollama.model)
|
|
@@ -11395,16 +12010,41 @@ class SessionState:
|
|
|
11395
12010
|
|
|
11396
12011
|
def _mark_all_done_silently(self, reason: str = "") -> dict:
|
|
11397
12012
|
summary = trim(str(reason or "").strip(), 160) or "arbiter: task completed"
|
|
12013
|
+
# Protect plan_step todos — they must only be completed by _advance_plan_step
|
|
12014
|
+
bb = self._ensure_blackboard()
|
|
12015
|
+
plan_todos = bb.get("project_todos", [])
|
|
12016
|
+
saved_plan_steps: dict[str, dict] = {}
|
|
12017
|
+
for t in plan_todos:
|
|
12018
|
+
if t.get("category") == "plan_step" and t.get("status") != "completed":
|
|
12019
|
+
saved_plan_steps[str(t.get("id", ""))] = {
|
|
12020
|
+
"status": t.get("status", "pending"),
|
|
12021
|
+
"completed_at": t.get("completed_at"),
|
|
12022
|
+
"completed_by": t.get("completed_by", ""),
|
|
12023
|
+
"evidence": t.get("evidence", ""),
|
|
12024
|
+
}
|
|
11398
12025
|
result = self.todo.complete_all_open(summary)
|
|
12026
|
+
# Restore plan_step todos to their original state
|
|
12027
|
+
if saved_plan_steps:
|
|
12028
|
+
bb = self._ensure_blackboard()
|
|
12029
|
+
for t in bb.get("project_todos", []):
|
|
12030
|
+
tid = str(t.get("id", ""))
|
|
12031
|
+
if tid in saved_plan_steps:
|
|
12032
|
+
t["status"] = saved_plan_steps[tid]["status"]
|
|
12033
|
+
t["completed_at"] = saved_plan_steps[tid]["completed_at"]
|
|
12034
|
+
t["completed_by"] = saved_plan_steps[tid]["completed_by"]
|
|
12035
|
+
t["evidence"] = saved_plan_steps[tid]["evidence"]
|
|
12036
|
+
self.blackboard = bb
|
|
12037
|
+
self._sync_todos_from_blackboard(reason="plan-step-restore")
|
|
11399
12038
|
updated = int(result.get("updated", 0) or 0)
|
|
11400
12039
|
total = int(result.get("total", 0) or 0)
|
|
12040
|
+
pending_steps = len(saved_plan_steps)
|
|
11401
12041
|
self._prune_runtime_retry_hints()
|
|
11402
12042
|
self._emit(
|
|
11403
12043
|
"status",
|
|
11404
12044
|
{
|
|
11405
12045
|
"summary": (
|
|
11406
|
-
"arbiter marked task completed; "
|
|
11407
|
-
f"
|
|
12046
|
+
f"arbiter marked task completed; todos completed={updated}/{total}"
|
|
12047
|
+
+ (f"; {pending_steps} plan steps protected (still pending)" if pending_steps else "")
|
|
11408
12048
|
)
|
|
11409
12049
|
},
|
|
11410
12050
|
)
|
|
@@ -12484,13 +13124,61 @@ class SessionState:
|
|
|
12484
13124
|
def _run_read(self, path: str, limit: int | None = None) -> str:
|
|
12485
13125
|
try:
|
|
12486
13126
|
rel = self._normalize_tool_path_text(path)
|
|
12487
|
-
|
|
13127
|
+
fp = self._session_path(rel)
|
|
13128
|
+
# Multimodal: detect image/audio/video files and handle natively
|
|
13129
|
+
ext = fp.suffix.lower() if fp.suffix else ""
|
|
13130
|
+
if ext in IMAGE_EXTS:
|
|
13131
|
+
return self._run_read_media(fp, rel, "image")
|
|
13132
|
+
if ext in AUDIO_EXTS:
|
|
13133
|
+
return self._run_read_media(fp, rel, "audio")
|
|
13134
|
+
if ext in VIDEO_EXTS:
|
|
13135
|
+
return self._run_read_media(fp, rel, "video")
|
|
13136
|
+
lines = fp.read_text(encoding="utf-8").splitlines()
|
|
12488
13137
|
if limit and limit < len(lines):
|
|
12489
13138
|
lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
|
|
12490
13139
|
return trim("\n".join(lines))
|
|
12491
13140
|
except Exception as exc:
|
|
12492
13141
|
return f"Error: {type(exc).__name__}: {exc}"
|
|
12493
13142
|
|
|
13143
|
+
def _run_read_media(self, fp: Path, rel: str, media_type: str) -> str:
|
|
13144
|
+
"""Read a media file and return description or inject into multimodal context."""
|
|
13145
|
+
if not fp.exists():
|
|
13146
|
+
return f"Error: file not found: {rel}"
|
|
13147
|
+
caps = self._capabilities_from_profile()
|
|
13148
|
+
cap_key = f"input_{media_type}"
|
|
13149
|
+
file_size = fp.stat().st_size
|
|
13150
|
+
size_kb = file_size / 1024
|
|
13151
|
+
if bool(caps.get(cap_key, False)) and file_size < 20 * 1024 * 1024:
|
|
13152
|
+
# Model supports this media type — encode and inject as multimodal input
|
|
13153
|
+
try:
|
|
13154
|
+
b64 = base64.b64encode(fp.read_bytes()).decode("ascii")
|
|
13155
|
+
mime = guess_mime_from_name(fp.name)
|
|
13156
|
+
media_item = {
|
|
13157
|
+
"type": media_type,
|
|
13158
|
+
"data_b64": b64,
|
|
13159
|
+
"mime": mime,
|
|
13160
|
+
"name": fp.name,
|
|
13161
|
+
"workspace_path": str(rel),
|
|
13162
|
+
}
|
|
13163
|
+
# Store for injection into next API call
|
|
13164
|
+
with self.lock:
|
|
13165
|
+
self._pending_media_inputs.append(media_item)
|
|
13166
|
+
return (
|
|
13167
|
+
f"[{media_type} file loaded: {rel} ({size_kb:.1f} KB, {mime})] "
|
|
13168
|
+
f"The {media_type} has been attached to the conversation for native {media_type} analysis. "
|
|
13169
|
+
f"Describe or analyze it directly."
|
|
13170
|
+
)
|
|
13171
|
+
except Exception as exc:
|
|
13172
|
+
return f"Error reading {media_type} file: {exc}"
|
|
13173
|
+
else:
|
|
13174
|
+
# Model doesn't support this media type — return metadata only
|
|
13175
|
+
reason = "model does not support" if not caps.get(cap_key) else "file too large"
|
|
13176
|
+
return (
|
|
13177
|
+
f"[{media_type} file: {rel} ({size_kb:.1f} KB)] "
|
|
13178
|
+
f"Note: {reason} native {media_type} input. "
|
|
13179
|
+
f"File exists at {fp}. Use bash tools to process it if needed."
|
|
13180
|
+
)
|
|
13181
|
+
|
|
12494
13182
|
def _is_html_file_rel(self, path: str) -> bool:
|
|
12495
13183
|
low = str(path or "").strip().lower()
|
|
12496
13184
|
return low.endswith(".html") or low.endswith(".htm")
|
|
@@ -13296,6 +13984,15 @@ class SessionState:
|
|
|
13296
13984
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13297
13985
|
if bool(self.runtime_goal_reset_pending):
|
|
13298
13986
|
return False, "goal-reset-pending"
|
|
13987
|
+
# Plan step gate: if plan_step todos exist, ALL must be completed
|
|
13988
|
+
plan_step_todos = [
|
|
13989
|
+
t for t in bb.get("project_todos", [])
|
|
13990
|
+
if t.get("category") == "plan_step"
|
|
13991
|
+
]
|
|
13992
|
+
if plan_step_todos:
|
|
13993
|
+
pending = [t for t in plan_step_todos if t.get("status") != "completed"]
|
|
13994
|
+
if pending:
|
|
13995
|
+
return False, f"plan-steps-incomplete:{len(pending)}/{len(plan_step_todos)}"
|
|
13299
13996
|
# Project todo gate: coding tasks must pass compile + test
|
|
13300
13997
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
13301
13998
|
task_type = str(profile.get("task_type", "general") or "general")
|
|
@@ -13354,8 +14051,16 @@ class SessionState:
|
|
|
13354
14051
|
|
|
13355
14052
|
def _manager_progress_state(self, board: dict | None = None) -> str:
|
|
13356
14053
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14054
|
+
# Hard guard: pending plan steps → never "done"
|
|
14055
|
+
plan_step_todos = [
|
|
14056
|
+
t for t in bb.get("project_todos", [])
|
|
14057
|
+
if t.get("category") == "plan_step"
|
|
14058
|
+
]
|
|
14059
|
+
has_pending_plan_steps = plan_step_todos and any(
|
|
14060
|
+
t.get("status") != "completed" for t in plan_step_todos
|
|
14061
|
+
)
|
|
13357
14062
|
can_finish, _ = self._can_auto_finish_from_approval(bb)
|
|
13358
|
-
if can_finish:
|
|
14063
|
+
if can_finish and not has_pending_plan_steps:
|
|
13359
14064
|
return "done"
|
|
13360
14065
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
13361
14066
|
task_type = str(profile.get("task_type", "") or "")
|
|
@@ -14005,10 +14710,13 @@ class SessionState:
|
|
|
14005
14710
|
bb = self._ensure_blackboard()
|
|
14006
14711
|
dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
|
|
14007
14712
|
trigger_reason = ""
|
|
14713
|
+
_is_single = self._effective_execution_mode() == EXECUTION_MODE_SINGLE
|
|
14714
|
+
_intent_th = WATCHDOG_INTENT_NO_TOOL_THRESHOLD_SINGLE if _is_single else WATCHDOG_INTENT_NO_TOOL_THRESHOLD
|
|
14715
|
+
_repeat_th = WATCHDOG_REPEAT_NO_TOOL_THRESHOLD_SINGLE if _is_single else WATCHDOG_REPEAT_NO_TOOL_THRESHOLD
|
|
14008
14716
|
if not bool(dq.get("active", False)):
|
|
14009
|
-
if int(wd.get("intent_no_tool_streak", 0) or 0) >= int(
|
|
14717
|
+
if int(wd.get("intent_no_tool_streak", 0) or 0) >= int(_intent_th):
|
|
14010
14718
|
trigger_reason = "intent-without-tool-call"
|
|
14011
|
-
elif int(wd.get("repeat_no_tool_streak", 0) or 0) >= int(
|
|
14719
|
+
elif int(wd.get("repeat_no_tool_streak", 0) or 0) >= int(_repeat_th):
|
|
14012
14720
|
trigger_reason = "repeated-no-tool-reply"
|
|
14013
14721
|
elif (
|
|
14014
14722
|
self._watchdog_context_near_limit()
|
|
@@ -14033,6 +14741,8 @@ class SessionState:
|
|
|
14033
14741
|
bb["watchdog"] = wd
|
|
14034
14742
|
self.blackboard = bb
|
|
14035
14743
|
self._blackboard_touch()
|
|
14744
|
+
# Failure ledger: record stall event
|
|
14745
|
+
self._ledger_record_stall(trigger_reason, "unresolved")
|
|
14036
14746
|
if int(wd["trigger_count"]) >= 2:
|
|
14037
14747
|
self._watchdog_escalate_to_single_developer(bb, reason=trigger_reason)
|
|
14038
14748
|
else:
|
|
@@ -14180,6 +14890,16 @@ class SessionState:
|
|
|
14180
14890
|
"project_todos": [],
|
|
14181
14891
|
"watchdog": self._new_watchdog_state(),
|
|
14182
14892
|
"decomposition_queue": self._new_decomposition_queue_state(),
|
|
14893
|
+
"failure_ledger": {
|
|
14894
|
+
"attempted_fixes": [],
|
|
14895
|
+
"compilation_errors": [],
|
|
14896
|
+
"errors": [],
|
|
14897
|
+
"repeated_delegations": [],
|
|
14898
|
+
"stall_events": [],
|
|
14899
|
+
"tool_call_fingerprints": [],
|
|
14900
|
+
},
|
|
14901
|
+
"checkpoints": [],
|
|
14902
|
+
"persisted_manager_routes": [],
|
|
14183
14903
|
}
|
|
14184
14904
|
|
|
14185
14905
|
def _normalize_blackboard(self, raw: object) -> dict:
|
|
@@ -14351,8 +15071,324 @@ class SessionState:
|
|
|
14351
15071
|
board["decomposition_queue"] = self._normalize_decomposition_queue_state(
|
|
14352
15072
|
src.get("decomposition_queue", {})
|
|
14353
15073
|
)
|
|
15074
|
+
# Preserve plan-mode data through normalization
|
|
15075
|
+
raw_plan = src.get("plan")
|
|
15076
|
+
if isinstance(raw_plan, dict):
|
|
15077
|
+
board["plan"] = raw_plan
|
|
15078
|
+
# Preserve failure_ledger, checkpoints, persisted_manager_routes
|
|
15079
|
+
raw_fl = src.get("failure_ledger")
|
|
15080
|
+
board["failure_ledger"] = self._normalize_failure_ledger(raw_fl) if isinstance(raw_fl, dict) else {
|
|
15081
|
+
"attempted_fixes": [],
|
|
15082
|
+
"compilation_errors": [],
|
|
15083
|
+
"errors": [],
|
|
15084
|
+
"repeated_delegations": [],
|
|
15085
|
+
"stall_events": [],
|
|
15086
|
+
"tool_call_fingerprints": [],
|
|
15087
|
+
}
|
|
15088
|
+
raw_cp = src.get("checkpoints")
|
|
15089
|
+
board["checkpoints"] = list(raw_cp)[-CHECKPOINT_MAX_COUNT:] if isinstance(raw_cp, list) else []
|
|
15090
|
+
raw_pmr = src.get("persisted_manager_routes")
|
|
15091
|
+
board["persisted_manager_routes"] = list(raw_pmr)[-PERSISTED_ROUTES_MAX:] if isinstance(raw_pmr, list) else []
|
|
14354
15092
|
return board
|
|
14355
15093
|
|
|
15094
|
+
def _normalize_failure_ledger(self, raw: dict) -> dict:
|
|
15095
|
+
fl: dict = {
|
|
15096
|
+
"attempted_fixes": [],
|
|
15097
|
+
"compilation_errors": [],
|
|
15098
|
+
"errors": [],
|
|
15099
|
+
"repeated_delegations": [],
|
|
15100
|
+
"stall_events": [],
|
|
15101
|
+
"tool_call_fingerprints": [],
|
|
15102
|
+
}
|
|
15103
|
+
if not isinstance(raw, dict):
|
|
15104
|
+
return fl
|
|
15105
|
+
def _safe_list(key: str, max_len: int) -> list[dict]:
|
|
15106
|
+
items = raw.get(key)
|
|
15107
|
+
if not isinstance(items, list):
|
|
15108
|
+
return []
|
|
15109
|
+
out: list[dict] = []
|
|
15110
|
+
for item in items[-max_len:]:
|
|
15111
|
+
if isinstance(item, dict):
|
|
15112
|
+
out.append(item)
|
|
15113
|
+
return out
|
|
15114
|
+
fl["attempted_fixes"] = _safe_list("attempted_fixes", FAILURE_LEDGER_MAX_FIXES)
|
|
15115
|
+
fl["compilation_errors"] = _safe_list("compilation_errors", FAILURE_LEDGER_MAX_COMPILE_ERRORS)
|
|
15116
|
+
fl["errors"] = _safe_list("errors", FAILURE_LEDGER_MAX_ERRORS)
|
|
15117
|
+
fl["repeated_delegations"] = _safe_list("repeated_delegations", FAILURE_LEDGER_MAX_DELEGATIONS)
|
|
15118
|
+
fl["stall_events"] = _safe_list("stall_events", FAILURE_LEDGER_MAX_STALLS)
|
|
15119
|
+
fl["tool_call_fingerprints"] = _safe_list("tool_call_fingerprints", FAILURE_LEDGER_MAX_TOOL_FPS)
|
|
15120
|
+
return fl
|
|
15121
|
+
|
|
15122
|
+
def _ledger_record_fix_attempt(self, file: str, error: str, fix: str, result: str, actor: str):
|
|
15123
|
+
bb = self._ensure_blackboard()
|
|
15124
|
+
fl = bb.get("failure_ledger")
|
|
15125
|
+
if not isinstance(fl, dict):
|
|
15126
|
+
return
|
|
15127
|
+
fixes = fl.get("attempted_fixes", [])
|
|
15128
|
+
fixes.append({
|
|
15129
|
+
"file": trim(str(file or ""), 260),
|
|
15130
|
+
"error": trim(str(error or ""), 400),
|
|
15131
|
+
"fix": trim(str(fix or ""), 400),
|
|
15132
|
+
"result": trim(str(result or ""), 200),
|
|
15133
|
+
"actor": trim(str(actor or ""), 40),
|
|
15134
|
+
"round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15135
|
+
"ts": float(now_ts()),
|
|
15136
|
+
})
|
|
15137
|
+
fl["attempted_fixes"] = fixes[-FAILURE_LEDGER_MAX_FIXES:]
|
|
15138
|
+
bb["failure_ledger"] = fl
|
|
15139
|
+
self.blackboard = bb
|
|
15140
|
+
|
|
15141
|
+
# --- Unified error detection helpers (修改 3) ---
|
|
15142
|
+
|
|
15143
|
+
def _detect_error_category(self, cmd_str: str) -> str | None:
|
|
15144
|
+
"""Match a command string against ERROR_CATEGORY_DEFS, return first matching category name."""
|
|
15145
|
+
cmd_lower = str(cmd_str or "").lower()
|
|
15146
|
+
for cat_name, cat_def in ERROR_CATEGORY_DEFS:
|
|
15147
|
+
if any(kw in cmd_lower for kw in cat_def.get("cmd_keywords", [])):
|
|
15148
|
+
return cat_name
|
|
15149
|
+
return None
|
|
15150
|
+
|
|
15151
|
+
def _check_output_has_errors(self, output: str, category: str) -> bool:
|
|
15152
|
+
"""Check if output contains error patterns for the given category."""
|
|
15153
|
+
out_lower = str(output or "").lower()
|
|
15154
|
+
cat_def = dict(ERROR_CATEGORY_DEFS).get(category, {})
|
|
15155
|
+
return any(pat.lower() in out_lower for pat in cat_def.get("output_patterns", []))
|
|
15156
|
+
|
|
15157
|
+
def _extract_error_lines(self, output: str, category: str, max_lines: int = 3) -> list[tuple[str, str]]:
|
|
15158
|
+
"""Scan output lines for error patterns, return [(file_hint, error_line), ...]."""
|
|
15159
|
+
cat_def = dict(ERROR_CATEGORY_DEFS).get(category, {})
|
|
15160
|
+
patterns = [p.lower() for p in cat_def.get("output_patterns", [])]
|
|
15161
|
+
results: list[tuple[str, str]] = []
|
|
15162
|
+
for ln in str(output or "").split("\n"):
|
|
15163
|
+
if any(pat in ln.lower() for pat in patterns):
|
|
15164
|
+
file_hint = ""
|
|
15165
|
+
parts = ln.split(":")
|
|
15166
|
+
if len(parts) >= 2 and ("." in parts[0] or "/" in parts[0]):
|
|
15167
|
+
file_hint = parts[0].strip()
|
|
15168
|
+
results.append((file_hint, ln.strip()))
|
|
15169
|
+
if len(results) >= max_lines:
|
|
15170
|
+
break
|
|
15171
|
+
return results
|
|
15172
|
+
|
|
15173
|
+
# --- Unified error record / clear / sync (修改 4) ---
|
|
15174
|
+
|
|
15175
|
+
def _ledger_record_error(self, category: str, file: str, error_msg: str):
|
|
15176
|
+
"""Record an error into fl['errors'] with category field. Dedup by fingerprint."""
|
|
15177
|
+
bb = self._ensure_blackboard()
|
|
15178
|
+
fl = bb.get("failure_ledger")
|
|
15179
|
+
if not isinstance(fl, dict):
|
|
15180
|
+
return
|
|
15181
|
+
errors = fl.get("errors", [])
|
|
15182
|
+
fp = hashlib.sha1((str(category or "") + str(file or "") + str(error_msg or "")).encode("utf-8")).hexdigest()[:12]
|
|
15183
|
+
for entry in errors:
|
|
15184
|
+
if entry.get("fingerprint") == fp:
|
|
15185
|
+
entry["count"] = int(entry.get("count", 1) or 1) + 1
|
|
15186
|
+
entry["last_round"] = int(getattr(self, "agent_round_index", 0) or 0)
|
|
15187
|
+
fl["errors"] = errors
|
|
15188
|
+
bb["failure_ledger"] = fl
|
|
15189
|
+
self.blackboard = bb
|
|
15190
|
+
self._sync_compilation_errors_compat()
|
|
15191
|
+
return
|
|
15192
|
+
errors.append({
|
|
15193
|
+
"fingerprint": fp,
|
|
15194
|
+
"category": str(category or ""),
|
|
15195
|
+
"file": trim(str(file or ""), 260),
|
|
15196
|
+
"error_msg": trim(str(error_msg or ""), 400),
|
|
15197
|
+
"count": 1,
|
|
15198
|
+
"first_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15199
|
+
"last_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15200
|
+
})
|
|
15201
|
+
fl["errors"] = errors[-FAILURE_LEDGER_MAX_ERRORS:]
|
|
15202
|
+
bb["failure_ledger"] = fl
|
|
15203
|
+
self.blackboard = bb
|
|
15204
|
+
self._sync_compilation_errors_compat()
|
|
15205
|
+
|
|
15206
|
+
def _ledger_clear_errors(self, category: str | None = None):
|
|
15207
|
+
"""Clear errors from fl['errors']. If category given, only that category; else all."""
|
|
15208
|
+
bb = self._ensure_blackboard()
|
|
15209
|
+
fl = bb.get("failure_ledger")
|
|
15210
|
+
if not isinstance(fl, dict):
|
|
15211
|
+
return
|
|
15212
|
+
if category is None:
|
|
15213
|
+
fl["errors"] = []
|
|
15214
|
+
fl["attempted_fixes"] = []
|
|
15215
|
+
else:
|
|
15216
|
+
fl["errors"] = [e for e in fl.get("errors", []) if e.get("category") != category]
|
|
15217
|
+
# Clear attempted_fixes that match files in the cleared category
|
|
15218
|
+
fl["attempted_fixes"] = [
|
|
15219
|
+
fx for fx in fl.get("attempted_fixes", [])
|
|
15220
|
+
if not (isinstance(fx, dict) and fx.get("category") == category)
|
|
15221
|
+
]
|
|
15222
|
+
bb["failure_ledger"] = fl
|
|
15223
|
+
self.blackboard = bb
|
|
15224
|
+
self._sync_compilation_errors_compat()
|
|
15225
|
+
|
|
15226
|
+
def _sync_compilation_errors_compat(self):
|
|
15227
|
+
"""Sync fl['compilation_errors'] from fl['errors'] where category=='compilation' for backward compat."""
|
|
15228
|
+
bb = self._ensure_blackboard()
|
|
15229
|
+
fl = bb.get("failure_ledger")
|
|
15230
|
+
if not isinstance(fl, dict):
|
|
15231
|
+
return
|
|
15232
|
+
fl["compilation_errors"] = [
|
|
15233
|
+
e for e in fl.get("errors", [])
|
|
15234
|
+
if isinstance(e, dict) and e.get("category") == "compilation"
|
|
15235
|
+
]
|
|
15236
|
+
bb["failure_ledger"] = fl
|
|
15237
|
+
self.blackboard = bb
|
|
15238
|
+
|
|
15239
|
+
# --- Thin wrappers for backward compatibility (修改 4d, 4e) ---
|
|
15240
|
+
|
|
15241
|
+
def _ledger_record_compile_error(self, file: str, error_msg: str):
|
|
15242
|
+
self._ledger_record_error("compilation", file, error_msg)
|
|
15243
|
+
|
|
15244
|
+
def _ledger_clear_compile_errors(self):
|
|
15245
|
+
self._ledger_clear_errors("compilation")
|
|
15246
|
+
|
|
15247
|
+
# --- Unified tool result error processing (修改 5) ---
|
|
15248
|
+
|
|
15249
|
+
def _process_tool_result_errors(self, name: str, args: dict, output: str, ok: bool, role_key: str):
|
|
15250
|
+
"""Detect and record errors from bash tool results, or clear on success."""
|
|
15251
|
+
if name != "bash" or not isinstance(args, dict):
|
|
15252
|
+
return
|
|
15253
|
+
cmd_str = str(args.get("command", "") or "").lower()
|
|
15254
|
+
category = self._detect_error_category(cmd_str)
|
|
15255
|
+
if not category:
|
|
15256
|
+
return
|
|
15257
|
+
if not ok:
|
|
15258
|
+
error_lines = self._extract_error_lines(str(output or ""), category)
|
|
15259
|
+
for file_hint, err_line in error_lines:
|
|
15260
|
+
self._ledger_record_error(category, file_hint or cmd_str[:80], trim(err_line, 300))
|
|
15261
|
+
else:
|
|
15262
|
+
out_lower = str(output or "").lower()
|
|
15263
|
+
cat_def = dict(ERROR_CATEGORY_DEFS).get(category, {})
|
|
15264
|
+
negatives = cat_def.get("success_negatives", [])
|
|
15265
|
+
if not any(neg.lower() in out_lower for neg in negatives):
|
|
15266
|
+
self._ledger_clear_errors(category)
|
|
15267
|
+
# Auto-deactivate reviewer debug mode when errors are resolved
|
|
15268
|
+
if bool(self.reviewer_debug_mode) and not self._manager_has_error_log():
|
|
15269
|
+
self._deactivate_reviewer_debug_mode("errors resolved")
|
|
15270
|
+
|
|
15271
|
+
# --- Generalized error context (修改 6) ---
|
|
15272
|
+
|
|
15273
|
+
def _recent_error_context(self, max_chars: int = 800) -> str:
|
|
15274
|
+
"""Build context string from all error categories in fl['errors']."""
|
|
15275
|
+
bb = self._ensure_blackboard()
|
|
15276
|
+
lines: list[str] = []
|
|
15277
|
+
fl = bb.get("failure_ledger", {})
|
|
15278
|
+
if isinstance(fl, dict):
|
|
15279
|
+
all_errors = fl.get("errors", [])
|
|
15280
|
+
unresolved = [e for e in all_errors if isinstance(e, dict) and int(e.get("count", 0) or 0) > 0]
|
|
15281
|
+
if unresolved:
|
|
15282
|
+
# Group by category
|
|
15283
|
+
by_cat: dict[str, list] = {}
|
|
15284
|
+
for e in unresolved:
|
|
15285
|
+
cat = str(e.get("category", "unknown") or "unknown")
|
|
15286
|
+
by_cat.setdefault(cat, []).append(e)
|
|
15287
|
+
cat_labels = dict(ERROR_CATEGORY_DEFS)
|
|
15288
|
+
for cat, errs in by_cat.items():
|
|
15289
|
+
label = cat_labels.get(cat, {}).get("label", cat.upper()) if cat in cat_labels else cat.upper()
|
|
15290
|
+
lines.append(f"UNRESOLVED {label.upper()}:")
|
|
15291
|
+
for e in errs[:5]:
|
|
15292
|
+
f = str(e.get("file", "") or "")
|
|
15293
|
+
msg = str(e.get("error_msg", "") or "")
|
|
15294
|
+
cnt = int(e.get("count", 1) or 1)
|
|
15295
|
+
lines.append(f" [{f}] {trim(msg, 200)} (seen {cnt}x)")
|
|
15296
|
+
fixes = fl.get("attempted_fixes", [])
|
|
15297
|
+
if fixes:
|
|
15298
|
+
lines.append("PREVIOUSLY ATTEMPTED FIXES (all failed):")
|
|
15299
|
+
for fx in fixes[-3:]:
|
|
15300
|
+
if isinstance(fx, dict):
|
|
15301
|
+
lines.append(f" - {trim(str(fx.get('fix', '') or ''), 120)} -> {trim(str(fx.get('result', '') or ''), 60)}")
|
|
15302
|
+
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
15303
|
+
_all_kw = ("error:", "fatal error", "syntax error", "compile error", "build failed",
|
|
15304
|
+
"traceback", "exception", "failed", "panic:", "FAIL:", "AssertionError")
|
|
15305
|
+
for entry in reversed(logs[-6:]):
|
|
15306
|
+
if not isinstance(entry, dict):
|
|
15307
|
+
continue
|
|
15308
|
+
txt = str(entry.get("content", "") or "")
|
|
15309
|
+
if any(kw in txt.lower() for kw in _all_kw):
|
|
15310
|
+
lines.append(f"LAST ERROR OUTPUT:\n{trim(txt, 400)}")
|
|
15311
|
+
break
|
|
15312
|
+
if not lines:
|
|
15313
|
+
return ""
|
|
15314
|
+
return trim("\n".join(lines), max_chars)
|
|
15315
|
+
|
|
15316
|
+
def _recent_compile_error_context(self, max_chars: int = 800) -> str:
|
|
15317
|
+
return self._recent_error_context(max_chars=max_chars)
|
|
15318
|
+
|
|
15319
|
+
def _ledger_record_delegation(self, target: str, instruction: str):
|
|
15320
|
+
bb = self._ensure_blackboard()
|
|
15321
|
+
fl = bb.get("failure_ledger")
|
|
15322
|
+
if not isinstance(fl, dict):
|
|
15323
|
+
return
|
|
15324
|
+
delegations = fl.get("repeated_delegations", [])
|
|
15325
|
+
fp = hashlib.sha1(str(instruction or "").encode("utf-8")).hexdigest()[:12]
|
|
15326
|
+
for entry in delegations:
|
|
15327
|
+
if entry.get("instruction_hash") == fp and entry.get("target") == target:
|
|
15328
|
+
entry["count"] = int(entry.get("count", 1) or 1) + 1
|
|
15329
|
+
entry["last_round"] = int(getattr(self, "agent_round_index", 0) or 0)
|
|
15330
|
+
fl["repeated_delegations"] = delegations
|
|
15331
|
+
bb["failure_ledger"] = fl
|
|
15332
|
+
self.blackboard = bb
|
|
15333
|
+
return
|
|
15334
|
+
delegations.append({
|
|
15335
|
+
"target": trim(str(target or ""), 40),
|
|
15336
|
+
"instruction_hash": fp,
|
|
15337
|
+
"instruction_preview": trim(str(instruction or ""), 200),
|
|
15338
|
+
"count": 1,
|
|
15339
|
+
"first_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15340
|
+
"last_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15341
|
+
})
|
|
15342
|
+
fl["repeated_delegations"] = delegations[-FAILURE_LEDGER_MAX_DELEGATIONS:]
|
|
15343
|
+
bb["failure_ledger"] = fl
|
|
15344
|
+
self.blackboard = bb
|
|
15345
|
+
|
|
15346
|
+
def _ledger_record_stall(self, reason: str, resolution: str):
|
|
15347
|
+
bb = self._ensure_blackboard()
|
|
15348
|
+
fl = bb.get("failure_ledger")
|
|
15349
|
+
if not isinstance(fl, dict):
|
|
15350
|
+
return
|
|
15351
|
+
stalls = fl.get("stall_events", [])
|
|
15352
|
+
stalls.append({
|
|
15353
|
+
"reason": trim(str(reason or ""), 200),
|
|
15354
|
+
"resolution": trim(str(resolution or ""), 200),
|
|
15355
|
+
"round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15356
|
+
"ts": float(now_ts()),
|
|
15357
|
+
})
|
|
15358
|
+
fl["stall_events"] = stalls[-FAILURE_LEDGER_MAX_STALLS:]
|
|
15359
|
+
bb["failure_ledger"] = fl
|
|
15360
|
+
self.blackboard = bb
|
|
15361
|
+
|
|
15362
|
+
def _ledger_record_tool_call(self, tool_name: str, args: dict):
|
|
15363
|
+
bb = self._ensure_blackboard()
|
|
15364
|
+
fl = bb.get("failure_ledger")
|
|
15365
|
+
if not isinstance(fl, dict):
|
|
15366
|
+
return
|
|
15367
|
+
fps = fl.get("tool_call_fingerprints", [])
|
|
15368
|
+
try:
|
|
15369
|
+
args_str = json_dumps(args) if isinstance(args, dict) else str(args or "")
|
|
15370
|
+
except Exception:
|
|
15371
|
+
args_str = str(args or "")
|
|
15372
|
+
fp = hashlib.sha1((str(tool_name or "") + args_str).encode("utf-8")).hexdigest()[:16]
|
|
15373
|
+
for entry in fps:
|
|
15374
|
+
if entry.get("fingerprint") == fp:
|
|
15375
|
+
entry["count"] = int(entry.get("count", 1) or 1) + 1
|
|
15376
|
+
entry["last_round"] = int(getattr(self, "agent_round_index", 0) or 0)
|
|
15377
|
+
fl["tool_call_fingerprints"] = fps
|
|
15378
|
+
bb["failure_ledger"] = fl
|
|
15379
|
+
self.blackboard = bb
|
|
15380
|
+
return
|
|
15381
|
+
fps.append({
|
|
15382
|
+
"fingerprint": fp,
|
|
15383
|
+
"tool_name": trim(str(tool_name or ""), 60),
|
|
15384
|
+
"count": 1,
|
|
15385
|
+
"first_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15386
|
+
"last_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
15387
|
+
})
|
|
15388
|
+
fl["tool_call_fingerprints"] = fps[-FAILURE_LEDGER_MAX_TOOL_FPS:]
|
|
15389
|
+
bb["failure_ledger"] = fl
|
|
15390
|
+
self.blackboard = bb
|
|
15391
|
+
|
|
14356
15392
|
def _ensure_blackboard(self) -> dict:
|
|
14357
15393
|
if not isinstance(self.blackboard, dict) or not self.blackboard:
|
|
14358
15394
|
self.blackboard = self._new_blackboard(self._latest_user_goal_text())
|
|
@@ -14709,6 +15745,31 @@ class SessionState:
|
|
|
14709
15745
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14710
15746
|
if bb.get("project_todos"):
|
|
14711
15747
|
return
|
|
15748
|
+
# 如果有已审批的 plan steps,优先用 plan steps 生成 todos
|
|
15749
|
+
plan = bb.get("plan", {})
|
|
15750
|
+
if isinstance(plan, dict) and plan.get("phase") == "executing" and plan.get("steps"):
|
|
15751
|
+
steps = plan["steps"]
|
|
15752
|
+
plan_todos = [
|
|
15753
|
+
{
|
|
15754
|
+
"id": f"pt:{i:03d}",
|
|
15755
|
+
"content": trim(str(s or "").strip(), 400),
|
|
15756
|
+
"status": "in_progress" if i == 0 else "pending",
|
|
15757
|
+
"category": "plan_step",
|
|
15758
|
+
"plan_step_index": i,
|
|
15759
|
+
"created_at": float(now_ts()),
|
|
15760
|
+
"completed_at": None,
|
|
15761
|
+
"completed_by": "",
|
|
15762
|
+
"evidence": "",
|
|
15763
|
+
}
|
|
15764
|
+
for i, s in enumerate(steps) if str(s or "").strip()
|
|
15765
|
+
][:10]
|
|
15766
|
+
if plan_todos:
|
|
15767
|
+
bb["project_todos"] = plan_todos
|
|
15768
|
+
bb["plan_step_cursor"] = 0
|
|
15769
|
+
bb["plan_step_total"] = len(plan_todos)
|
|
15770
|
+
self.blackboard = bb
|
|
15771
|
+
self._blackboard_touch()
|
|
15772
|
+
return
|
|
14712
15773
|
raw = self._generate_project_todos_from_profile(bb)
|
|
14713
15774
|
bb["project_todos"] = [
|
|
14714
15775
|
{
|
|
@@ -14786,6 +15847,23 @@ class SessionState:
|
|
|
14786
15847
|
todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
|
|
14787
15848
|
elif cat == "review" and feedback_pass:
|
|
14788
15849
|
todo.update(status="completed", completed_at=float(now_ts()), evidence="审查通过")
|
|
15850
|
+
elif cat == "plan_step":
|
|
15851
|
+
# Plan steps 不自动完成,由 _advance_plan_step 显式推进
|
|
15852
|
+
# 但如果当前步骤之前的所有步骤都完成了,标记当前步骤为 in_progress
|
|
15853
|
+
if todo.get("status") == "pending":
|
|
15854
|
+
step_idx = int(todo.get("plan_step_index", 0) or 0)
|
|
15855
|
+
all_prior_done = all(
|
|
15856
|
+
t.get("status") == "completed"
|
|
15857
|
+
for t in todos
|
|
15858
|
+
if t.get("category") == "plan_step"
|
|
15859
|
+
and int(t.get("plan_step_index", 0) or 0) < step_idx
|
|
15860
|
+
)
|
|
15861
|
+
if all_prior_done and not any(
|
|
15862
|
+
t.get("status") == "in_progress"
|
|
15863
|
+
for t in todos
|
|
15864
|
+
if t.get("category") == "plan_step"
|
|
15865
|
+
):
|
|
15866
|
+
todo["status"] = "in_progress"
|
|
14789
15867
|
|
|
14790
15868
|
if not any(t.get("status") == "in_progress" for t in todos):
|
|
14791
15869
|
for t in todos:
|
|
@@ -14796,6 +15874,104 @@ class SessionState:
|
|
|
14796
15874
|
bb["project_todos"] = todos
|
|
14797
15875
|
self.blackboard = bb
|
|
14798
15876
|
|
|
15877
|
+
def _advance_plan_step(self, evidence: str = "", actor: str = "developer"):
|
|
15878
|
+
bb = self._ensure_blackboard()
|
|
15879
|
+
todos = bb.get("project_todos", [])
|
|
15880
|
+
if not todos:
|
|
15881
|
+
return False
|
|
15882
|
+
current = None
|
|
15883
|
+
for t in todos:
|
|
15884
|
+
if t.get("category") == "plan_step" and t.get("status") == "in_progress":
|
|
15885
|
+
current = t
|
|
15886
|
+
break
|
|
15887
|
+
if not current:
|
|
15888
|
+
return False
|
|
15889
|
+
current["status"] = "completed"
|
|
15890
|
+
current["completed_at"] = float(now_ts())
|
|
15891
|
+
current["completed_by"] = actor
|
|
15892
|
+
current["evidence"] = trim(str(evidence or "").strip(), 200) or "step completed"
|
|
15893
|
+
# 推进 cursor,激活下一步
|
|
15894
|
+
cursor = int(bb.get("plan_step_cursor", 0) or 0)
|
|
15895
|
+
bb["plan_step_cursor"] = cursor + 1
|
|
15896
|
+
next_step = None
|
|
15897
|
+
for t in todos:
|
|
15898
|
+
if t.get("category") == "plan_step" and t.get("status") == "pending":
|
|
15899
|
+
next_step = t
|
|
15900
|
+
break
|
|
15901
|
+
if next_step:
|
|
15902
|
+
next_step["status"] = "in_progress"
|
|
15903
|
+
step_idx = int(next_step.get("plan_step_index", 0) or 0) + 1
|
|
15904
|
+
total = int(bb.get("plan_step_total", len(todos)) or len(todos))
|
|
15905
|
+
self._emit("status", {
|
|
15906
|
+
"summary": f"📋 Plan step {step_idx}/{total}: {trim(str(next_step.get('content', '') or ''), 120)}"
|
|
15907
|
+
})
|
|
15908
|
+
self.blackboard = bb
|
|
15909
|
+
self._blackboard_touch()
|
|
15910
|
+
# Immediately sync todos so UI reflects plan step advancement
|
|
15911
|
+
self._sync_todos_from_blackboard(reason=f"plan-step-advanced:{cursor + 1}", board=bb)
|
|
15912
|
+
return True
|
|
15913
|
+
|
|
15914
|
+
def _single_agent_plan_step_check(self, tool_results: list[dict]):
|
|
15915
|
+
"""In single-agent mode, check if current plan step should be advanced based on tool results."""
|
|
15916
|
+
bb = self._ensure_blackboard()
|
|
15917
|
+
todos = bb.get("project_todos", [])
|
|
15918
|
+
plan_steps = [t for t in todos if t.get("category") == "plan_step"]
|
|
15919
|
+
if not plan_steps:
|
|
15920
|
+
# No plan steps — just sync todos
|
|
15921
|
+
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
15922
|
+
return
|
|
15923
|
+
current = None
|
|
15924
|
+
for t in plan_steps:
|
|
15925
|
+
if t.get("status") == "in_progress":
|
|
15926
|
+
current = t
|
|
15927
|
+
break
|
|
15928
|
+
if not current:
|
|
15929
|
+
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
15930
|
+
return
|
|
15931
|
+
# Heuristic: check if tool results indicate step completion
|
|
15932
|
+
# - write_file/edit_file calls suggest implementation progress
|
|
15933
|
+
# - successful bash calls suggest testing/verification
|
|
15934
|
+
step_content = str(current.get("content", "") or "").lower()
|
|
15935
|
+
phase = self._plan_step_phase_hint(step_content)
|
|
15936
|
+
wrote_files = any(
|
|
15937
|
+
str(r.get("name", "")) in ("write_file", "edit_file") and r.get("ok", False)
|
|
15938
|
+
for r in tool_results
|
|
15939
|
+
)
|
|
15940
|
+
ran_bash_ok = any(
|
|
15941
|
+
str(r.get("name", "")) == "bash" and r.get("ok", False)
|
|
15942
|
+
for r in tool_results
|
|
15943
|
+
)
|
|
15944
|
+
# Auto-advance conditions based on phase:
|
|
15945
|
+
should_advance = False
|
|
15946
|
+
if phase in ("research", "design") and wrote_files:
|
|
15947
|
+
# Research/design phase completed when files are produced
|
|
15948
|
+
should_advance = True
|
|
15949
|
+
elif phase == "implement" and wrote_files and ran_bash_ok:
|
|
15950
|
+
# Implementation completed when files written and bash succeeds
|
|
15951
|
+
should_advance = True
|
|
15952
|
+
elif phase in ("test", "review") and ran_bash_ok and not any(
|
|
15953
|
+
not r.get("ok", False) for r in tool_results if str(r.get("name", "")) == "bash"
|
|
15954
|
+
):
|
|
15955
|
+
# Test/review completed when all bash calls succeed
|
|
15956
|
+
should_advance = True
|
|
15957
|
+
# Also check if the agent explicitly mentioned step completion
|
|
15958
|
+
if not should_advance:
|
|
15959
|
+
# Check last assistant message for step completion signals
|
|
15960
|
+
last_text = ""
|
|
15961
|
+
for msg in reversed(self.messages[-3:]):
|
|
15962
|
+
if msg.get("role") == "assistant":
|
|
15963
|
+
last_text = str(msg.get("content", "") or "").lower()
|
|
15964
|
+
break
|
|
15965
|
+
step_done_signals = ("step completed", "步骤完成", "step done", "完成了", "已完成",
|
|
15966
|
+
"next step", "下一步", "proceed to step", "进入下一")
|
|
15967
|
+
if any(sig in last_text for sig in step_done_signals):
|
|
15968
|
+
should_advance = True
|
|
15969
|
+
if should_advance:
|
|
15970
|
+
evidence = f"single-agent auto-advance: phase={phase}, wrote={wrote_files}, bash_ok={ran_bash_ok}"
|
|
15971
|
+
self._advance_plan_step(evidence=evidence, actor="single")
|
|
15972
|
+
else:
|
|
15973
|
+
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
15974
|
+
|
|
14799
15975
|
def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
|
|
14800
15976
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14801
15977
|
todos = bb.get("project_todos", [])
|
|
@@ -14861,6 +16037,42 @@ class SessionState:
|
|
|
14861
16037
|
self._blackboard_touch()
|
|
14862
16038
|
self._sync_todos_from_blackboard(reason="approved", board=board)
|
|
14863
16039
|
|
|
16040
|
+
# --- Reviewer Debug Mode ---
|
|
16041
|
+
|
|
16042
|
+
def _activate_reviewer_debug_mode(self, error_context: str = ""):
|
|
16043
|
+
"""Activate reviewer debug mode — reviewer gets write access to fix bugs."""
|
|
16044
|
+
if bool(self.reviewer_debug_mode):
|
|
16045
|
+
return # already active
|
|
16046
|
+
self.reviewer_debug_mode = True
|
|
16047
|
+
self.reviewer_debug_rounds = 0
|
|
16048
|
+
self.reviewer_debug_context = trim(str(error_context or ""), 1000)
|
|
16049
|
+
self._emit("status", {
|
|
16050
|
+
"summary": "reviewer debug mode ACTIVATED — reviewer can now edit files to fix bugs"
|
|
16051
|
+
})
|
|
16052
|
+
|
|
16053
|
+
def _deactivate_reviewer_debug_mode(self, reason: str = ""):
|
|
16054
|
+
"""Deactivate reviewer debug mode — reviewer returns to read-only."""
|
|
16055
|
+
if not bool(self.reviewer_debug_mode):
|
|
16056
|
+
return
|
|
16057
|
+
self.reviewer_debug_mode = False
|
|
16058
|
+
rounds_used = int(self.reviewer_debug_rounds)
|
|
16059
|
+
self.reviewer_debug_rounds = 0
|
|
16060
|
+
self.reviewer_debug_context = ""
|
|
16061
|
+
self._emit("status", {
|
|
16062
|
+
"summary": f"reviewer debug mode DEACTIVATED ({reason or 'resolved'}; {rounds_used} rounds used)"
|
|
16063
|
+
})
|
|
16064
|
+
|
|
16065
|
+
def _check_reviewer_debug_mode_timeout(self) -> bool:
|
|
16066
|
+
"""Check if debug mode should be deactivated due to round limit.
|
|
16067
|
+
Returns True if debug mode was deactivated (should switch to developer)."""
|
|
16068
|
+
if not bool(self.reviewer_debug_mode):
|
|
16069
|
+
return False
|
|
16070
|
+
self.reviewer_debug_rounds = int(self.reviewer_debug_rounds or 0) + 1
|
|
16071
|
+
if self.reviewer_debug_rounds > REVIEWER_DEBUG_MODE_MAX_ROUNDS:
|
|
16072
|
+
self._deactivate_reviewer_debug_mode("round limit exceeded")
|
|
16073
|
+
return True
|
|
16074
|
+
return False
|
|
16075
|
+
|
|
14864
16076
|
def _auto_summary_on_finish(self) -> str:
|
|
14865
16077
|
"""Generate concise summary from blackboard state when run ends."""
|
|
14866
16078
|
bb = self._ensure_blackboard()
|
|
@@ -15015,7 +16227,9 @@ class SessionState:
|
|
|
15015
16227
|
|
|
15016
16228
|
proj_todos = board.get("project_todos", [])
|
|
15017
16229
|
if proj_todos:
|
|
15018
|
-
|
|
16230
|
+
has_plan_steps = any(pt.get("category") == "plan_step" for pt in proj_todos)
|
|
16231
|
+
title = "Plan Steps" if has_plan_steps else "Project Tasks"
|
|
16232
|
+
lines.append(f"\n### {title}")
|
|
15019
16233
|
for pt in proj_todos:
|
|
15020
16234
|
s = pt.get("status", "pending")
|
|
15021
16235
|
c = trim(str(pt.get("content", "") or ""), 200)
|
|
@@ -15026,8 +16240,9 @@ class SessionState:
|
|
|
15026
16240
|
mark = "[>]"
|
|
15027
16241
|
else:
|
|
15028
16242
|
mark = "[ ]"
|
|
16243
|
+
prefix = f"Step {int(pt.get('plan_step_index', 0) or 0) + 1}: " if has_plan_steps else ""
|
|
15029
16244
|
suffix = f" — {ev}" if ev else ""
|
|
15030
|
-
lines.append(f"- {mark} {c}{suffix}")
|
|
16245
|
+
lines.append(f"- {mark} {prefix}{c}{suffix}")
|
|
15031
16246
|
|
|
15032
16247
|
return "\n".join(lines)
|
|
15033
16248
|
|
|
@@ -15052,6 +16267,10 @@ class SessionState:
|
|
|
15052
16267
|
"requires_user_confirmation": {"type": "boolean"},
|
|
15053
16268
|
"is_mandatory": {"type": "boolean"},
|
|
15054
16269
|
"executor_mode": {"type": "boolean"},
|
|
16270
|
+
"advance_plan_step": {
|
|
16271
|
+
"type": "boolean",
|
|
16272
|
+
"description": "Set true if the current plan step is completed and should advance to next step.",
|
|
16273
|
+
},
|
|
15055
16274
|
},
|
|
15056
16275
|
["target", "instruction"],
|
|
15057
16276
|
)
|
|
@@ -15080,6 +16299,15 @@ class SessionState:
|
|
|
15080
16299
|
"participants": {"type": "array", "items": {"type": "string", "enum": list(AGENT_ROLES)}},
|
|
15081
16300
|
"assigned_expert": {"type": "string", "enum": list(AGENT_ROLES)},
|
|
15082
16301
|
"requires_user_confirmation": {"type": "boolean"},
|
|
16302
|
+
"requires_plan": {
|
|
16303
|
+
"type": "boolean",
|
|
16304
|
+
"description": (
|
|
16305
|
+
"Whether this task needs a plan-mode research phase before execution. "
|
|
16306
|
+
"True for tasks involving: multi-file refactoring, architecture changes, "
|
|
16307
|
+
"migration, new feature with unclear scope, system-level changes. "
|
|
16308
|
+
"False for simple Q&A, single-file edits, direct bug fixes."
|
|
16309
|
+
),
|
|
16310
|
+
},
|
|
15083
16311
|
},
|
|
15084
16312
|
["level", "judgement", "inherit_previous_state"],
|
|
15085
16313
|
)
|
|
@@ -15353,6 +16581,11 @@ class SessionState:
|
|
|
15353
16581
|
level = 3
|
|
15354
16582
|
if level not in TASK_LEVEL_CHOICES:
|
|
15355
16583
|
level = 3
|
|
16584
|
+
# Complexity inheritance: if user didn't mention complexity, inherit previous level
|
|
16585
|
+
previous_level = int(getattr(self, "runtime_task_level", 0) or 0)
|
|
16586
|
+
if previous_level > 0 and not self._user_mentions_complexity(str(goal_text or "")):
|
|
16587
|
+
if bool(inherit_previous_state) or self._is_plan_choice_response(str(goal_text or "")):
|
|
16588
|
+
level = previous_level
|
|
15356
16589
|
policy = dict(TASK_LEVEL_POLICIES.get(level, TASK_LEVEL_POLICIES[3]))
|
|
15357
16590
|
policy_mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
|
|
15358
16591
|
config_mode = normalize_execution_mode(self.execution_mode, default="")
|
|
@@ -15485,7 +16718,9 @@ class SessionState:
|
|
|
15485
16718
|
objective = trim(str(row.get("direct_objective", "") or "").strip(), 800)
|
|
15486
16719
|
if not objective:
|
|
15487
16720
|
objective = fallback_objective
|
|
16721
|
+
_prev_level_val = int(getattr(self, '_prev_applied_task_level', 0) or 0)
|
|
15488
16722
|
self.runtime_task_level = int(level)
|
|
16723
|
+
self._prev_applied_task_level = int(level)
|
|
15489
16724
|
self.runtime_execution_mode = mode
|
|
15490
16725
|
self.runtime_assigned_expert = assigned
|
|
15491
16726
|
self.runtime_participants = list(participants)
|
|
@@ -15499,6 +16734,32 @@ class SessionState:
|
|
|
15499
16734
|
self.runtime_direct_objective = objective
|
|
15500
16735
|
self.runtime_reclassify_goal = trim(str(goal_text or "").strip(), 4000)
|
|
15501
16736
|
self.runtime_reclassify_required = False
|
|
16737
|
+
# Plan mode 判定(用户偏好 > 规则兜底 > LLM 语义)
|
|
16738
|
+
raw_requires_plan = bool(decision.get("requires_plan", False))
|
|
16739
|
+
user_pref = str(self.plan_mode_user_preference or "auto").lower()
|
|
16740
|
+
if user_pref == "off":
|
|
16741
|
+
requires_plan = False
|
|
16742
|
+
elif user_pref == "on":
|
|
16743
|
+
requires_plan = True
|
|
16744
|
+
else: # auto — 保持原逻辑
|
|
16745
|
+
if level in PLAN_MODE_FORCED_LEVELS:
|
|
16746
|
+
requires_plan = True
|
|
16747
|
+
elif level in PLAN_MODE_ENABLED_LEVELS:
|
|
16748
|
+
requires_plan = raw_requires_plan
|
|
16749
|
+
else:
|
|
16750
|
+
requires_plan = False
|
|
16751
|
+
if self.runtime_plan_approved:
|
|
16752
|
+
requires_plan = False
|
|
16753
|
+
self.runtime_plan_mode_needed = requires_plan
|
|
16754
|
+
# Check if user is replying with a plan choice
|
|
16755
|
+
if self.runtime_plan_proposal and not self.runtime_plan_approved:
|
|
16756
|
+
choice_text = str(goal_text or "").strip()
|
|
16757
|
+
choice = self._parse_plan_choice(choice_text, self.runtime_plan_proposal)
|
|
16758
|
+
if choice:
|
|
16759
|
+
self.runtime_plan_choice = choice
|
|
16760
|
+
self.runtime_plan_approved = True
|
|
16761
|
+
self.runtime_plan_mode_needed = False
|
|
16762
|
+
self._inject_plan_into_context(choice)
|
|
15502
16763
|
board = self._ensure_blackboard()
|
|
15503
16764
|
profile = self._ensure_blackboard_task_profile(board)
|
|
15504
16765
|
profile["task_level"] = int(level)
|
|
@@ -15548,6 +16809,13 @@ class SessionState:
|
|
|
15548
16809
|
)
|
|
15549
16810
|
},
|
|
15550
16811
|
)
|
|
16812
|
+
if _prev_level_val > 0 and _prev_level_val != int(level):
|
|
16813
|
+
self._emit("status", {
|
|
16814
|
+
"summary": (
|
|
16815
|
+
f"⚠️ 复杂度等级变更: L{_prev_level_val} → L{level} "
|
|
16816
|
+
f"(理由: {trim(judgement, 200)})"
|
|
16817
|
+
)
|
|
16818
|
+
})
|
|
15551
16819
|
|
|
15552
16820
|
def _manager_classify_task_level(
|
|
15553
16821
|
self,
|
|
@@ -15579,6 +16847,15 @@ class SessionState:
|
|
|
15579
16847
|
"IMPORTANT: User has configured Single-agent mode. "
|
|
15580
16848
|
"Prefer level 1-2 for simple tasks. Only escalate to level 3+ if truly complex.\n"
|
|
15581
16849
|
)
|
|
16850
|
+
prev_level = int(self.runtime_task_level or 0)
|
|
16851
|
+
if prev_level > 0 and self._is_minor_followup_input(goal_text):
|
|
16852
|
+
prompt += (
|
|
16853
|
+
f"\nIMPORTANT: The previous task was classified as level {prev_level}. "
|
|
16854
|
+
f"This appears to be a follow-up input, NOT a new task. "
|
|
16855
|
+
f"Unless the user is clearly starting a completely different task, "
|
|
16856
|
+
f"you MUST inherit the previous level={prev_level} and set inherit_previous_state=true. "
|
|
16857
|
+
f"Do NOT downgrade the level for follow-up inputs.\n"
|
|
16858
|
+
)
|
|
15582
16859
|
with self.lock:
|
|
15583
16860
|
self.current_phase = "manager:classify:model-call"
|
|
15584
16861
|
self.current_tool_name = ""
|
|
@@ -15624,6 +16901,61 @@ class SessionState:
|
|
|
15624
16901
|
row["low_confidence_reason"] = "manager classifier returned no valid tool call"
|
|
15625
16902
|
return row
|
|
15626
16903
|
|
|
16904
|
+
# ------------------------------------------------------------------
|
|
16905
|
+
# Continuation / follow-up detection (complexity inheritance)
|
|
16906
|
+
# ------------------------------------------------------------------
|
|
16907
|
+
|
|
16908
|
+
_CONTINUATION_PHRASES = frozenset({
|
|
16909
|
+
"继续", "continue", "go", "proceed", "go ahead", "ok", "好", "好的",
|
|
16910
|
+
"是", "是的", "确认", "执行", "开始", "start", "yes", "do it", "run it",
|
|
16911
|
+
"let's go", "carry on", "keep going", "接着做", "往下做", "下一步", "next",
|
|
16912
|
+
})
|
|
16913
|
+
|
|
16914
|
+
def _is_continuation_input(self, text: str) -> bool:
|
|
16915
|
+
"""Return True if *text* is a short continuation phrase that should inherit complexity."""
|
|
16916
|
+
t = str(text or "").strip().lower().rstrip("!.。!")
|
|
16917
|
+
if not t or len(t) > 30:
|
|
16918
|
+
return False
|
|
16919
|
+
if "?" in t or "?" in t or "\n" in t:
|
|
16920
|
+
return False
|
|
16921
|
+
return t in self._CONTINUATION_PHRASES
|
|
16922
|
+
|
|
16923
|
+
_NEW_TASK_PREFIXES = (
|
|
16924
|
+
"实现", "创建", "写一个", "新建", "添加", "开发", "设计", "重构", "修改",
|
|
16925
|
+
"build", "implement", "create", "add", "write", "develop", "design",
|
|
16926
|
+
"refactor", "make", "generate", "set up", "setup",
|
|
16927
|
+
)
|
|
16928
|
+
|
|
16929
|
+
def _is_minor_followup_input(self, text: str) -> bool:
|
|
16930
|
+
"""Return True if *text* looks like a lightweight follow-up, not a brand-new task."""
|
|
16931
|
+
t = str(text or "").strip()
|
|
16932
|
+
if not t or len(t) > 80:
|
|
16933
|
+
return False
|
|
16934
|
+
t_lower = t.lower()
|
|
16935
|
+
for prefix in self._NEW_TASK_PREFIXES:
|
|
16936
|
+
if t_lower.startswith(prefix):
|
|
16937
|
+
return False
|
|
16938
|
+
board = self._ensure_blackboard()
|
|
16939
|
+
progress = str(board.get("progress", "") or "").strip().lower()
|
|
16940
|
+
if progress in ("done", "completed"):
|
|
16941
|
+
return False
|
|
16942
|
+
return True
|
|
16943
|
+
|
|
16944
|
+
def _is_plan_choice_response(self, text: str) -> bool:
|
|
16945
|
+
"""Check if user text is a response to a plan proposal."""
|
|
16946
|
+
bb = self._ensure_blackboard()
|
|
16947
|
+
plan = bb.get("plan", {})
|
|
16948
|
+
if not isinstance(plan, dict):
|
|
16949
|
+
return False
|
|
16950
|
+
if plan.get("phase") != "awaiting_choice":
|
|
16951
|
+
return False
|
|
16952
|
+
return True
|
|
16953
|
+
|
|
16954
|
+
def _user_mentions_complexity(self, text: str) -> bool:
|
|
16955
|
+
"""Check if user text explicitly mentions complexity-related keywords."""
|
|
16956
|
+
t = str(text or "").strip().lower()
|
|
16957
|
+
return any(kw in t for kw in COMPLEXITY_KEYWORDS)
|
|
16958
|
+
|
|
15627
16959
|
def _refresh_runtime_task_policy(
|
|
15628
16960
|
self,
|
|
15629
16961
|
*,
|
|
@@ -15635,6 +16967,26 @@ class SessionState:
|
|
|
15635
16967
|
goal = trim(str(goal_text or self.runtime_reclassify_goal or self._latest_user_goal_text() or "").strip(), 4000)
|
|
15636
16968
|
if not goal:
|
|
15637
16969
|
return {}
|
|
16970
|
+
# --- Continuation input short-circuit: inherit previous complexity ---
|
|
16971
|
+
prev_level = int(self.runtime_task_level or 0)
|
|
16972
|
+
if prev_level > 0 and self._is_continuation_input(goal):
|
|
16973
|
+
decision = {
|
|
16974
|
+
"level": prev_level,
|
|
16975
|
+
"execution_mode": self._effective_execution_mode(),
|
|
16976
|
+
"participants": list(self.runtime_participants or []),
|
|
16977
|
+
"assigned_expert": str(self.runtime_assigned_expert or ""),
|
|
16978
|
+
"task_type": str(self.runtime_task_type or "general"),
|
|
16979
|
+
"complexity": str(self.runtime_task_complexity or "simple"),
|
|
16980
|
+
"scale_preference": str(self.runtime_scale_preference or "balanced"),
|
|
16981
|
+
"round_budget": int(self.runtime_round_budget or 0),
|
|
16982
|
+
"requires_user_confirmation": False,
|
|
16983
|
+
"inherit_previous_state": True,
|
|
16984
|
+
"judgement": "continuation input detected — inheriting previous complexity",
|
|
16985
|
+
"source": "continuation-inherit",
|
|
16986
|
+
"semantic_confidence": "high",
|
|
16987
|
+
}
|
|
16988
|
+
self._apply_runtime_task_decision(goal, decision)
|
|
16989
|
+
return dict(decision)
|
|
15638
16990
|
need = bool(
|
|
15639
16991
|
force
|
|
15640
16992
|
or self.runtime_reclassify_required
|
|
@@ -15661,11 +17013,94 @@ class SessionState:
|
|
|
15661
17013
|
self._apply_runtime_task_decision(goal, decision)
|
|
15662
17014
|
return dict(decision or {})
|
|
15663
17015
|
|
|
17016
|
+
def _plan_steps_context_for_manager(self) -> str:
|
|
17017
|
+
bb = self._ensure_blackboard()
|
|
17018
|
+
todos = bb.get("project_todos", [])
|
|
17019
|
+
if not todos or not any(t.get("category") == "plan_step" for t in todos):
|
|
17020
|
+
return ""
|
|
17021
|
+
lines = ["APPROVED PLAN STEPS:"]
|
|
17022
|
+
for t in todos:
|
|
17023
|
+
if t.get("category") != "plan_step":
|
|
17024
|
+
continue
|
|
17025
|
+
idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
17026
|
+
status = t.get("status", "pending")
|
|
17027
|
+
mark = "✅" if status == "completed" else "👉" if status == "in_progress" else "⬜"
|
|
17028
|
+
phase_hint = self._plan_step_phase_hint(str(t.get("content", "") or ""))
|
|
17029
|
+
phase_tag = f" [{phase_hint}]" if phase_hint else ""
|
|
17030
|
+
lines.append(f" {mark} Step {idx}: {trim(str(t.get('content', '') or ''), 160)}{phase_tag}")
|
|
17031
|
+
lines.append("Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing. ")
|
|
17032
|
+
# Add current phase routing hint
|
|
17033
|
+
current_phase = self._infer_current_phase_from_blackboard()
|
|
17034
|
+
if current_phase:
|
|
17035
|
+
preferred = TASK_PHASE_ROUTING.get(current_phase, "developer")
|
|
17036
|
+
lines.append(
|
|
17037
|
+
f"CURRENT PHASE: {current_phase} → prefer routing to {preferred}. "
|
|
17038
|
+
f"Each phase has its own expertise — let {preferred} lead this phase independently. "
|
|
17039
|
+
"Do NOT carry over implementation patterns from previous phases. "
|
|
17040
|
+
)
|
|
17041
|
+
return "\n".join(lines) + "\n"
|
|
17042
|
+
|
|
17043
|
+
def _plan_step_phase_hint(self, step_content: str) -> str:
|
|
17044
|
+
"""Infer the task phase from a plan step's content."""
|
|
17045
|
+
c = str(step_content or "").lower()
|
|
17046
|
+
if any(kw in c for kw in ("研究", "分析", "调研", "探索", "research", "analyze", "investigate", "explore", "inspect")):
|
|
17047
|
+
return "research"
|
|
17048
|
+
if any(kw in c for kw in ("设计", "架构", "规划", "design", "architect", "plan", "interface", "接口")):
|
|
17049
|
+
return "design"
|
|
17050
|
+
if any(kw in c for kw in ("实现", "编写", "创建", "开发", "implement", "write", "create", "build", "develop", "code")):
|
|
17051
|
+
return "implement"
|
|
17052
|
+
if any(kw in c for kw in ("测试", "验证", "检查", "test", "verify", "check", "validate", "compile")):
|
|
17053
|
+
return "test"
|
|
17054
|
+
if any(kw in c for kw in ("审查", "评审", "review", "audit", "inspect code")):
|
|
17055
|
+
return "review"
|
|
17056
|
+
if any(kw in c for kw in ("部署", "发布", "deploy", "release", "publish", "配置")):
|
|
17057
|
+
return "deploy"
|
|
17058
|
+
return ""
|
|
17059
|
+
|
|
17060
|
+
def _infer_current_phase_from_blackboard(self) -> str:
|
|
17061
|
+
"""Infer the current task phase from blackboard state and active plan step."""
|
|
17062
|
+
bb = self._ensure_blackboard()
|
|
17063
|
+
for t in bb.get("project_todos", []):
|
|
17064
|
+
if t.get("category") == "plan_step" and t.get("status") == "in_progress":
|
|
17065
|
+
phase = self._plan_step_phase_hint(str(t.get("content", "") or ""))
|
|
17066
|
+
if phase:
|
|
17067
|
+
return phase
|
|
17068
|
+
# Fallback: infer from blackboard state
|
|
17069
|
+
code_count = len(bb.get("code_artifacts", {}) or {})
|
|
17070
|
+
research_count = len(bb.get("research_notes", []) or [])
|
|
17071
|
+
feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
|
|
17072
|
+
if not research_count and not code_count:
|
|
17073
|
+
return "research"
|
|
17074
|
+
if research_count > 0 and not code_count:
|
|
17075
|
+
return "design"
|
|
17076
|
+
if code_count > 0 and not feedback:
|
|
17077
|
+
return "implement"
|
|
17078
|
+
if feedback:
|
|
17079
|
+
return "review"
|
|
17080
|
+
return ""
|
|
17081
|
+
|
|
15664
17082
|
def _project_todo_hint_for_manager(self) -> str:
|
|
15665
17083
|
bb = self._ensure_blackboard()
|
|
15666
17084
|
todos = bb.get("project_todos", [])
|
|
15667
17085
|
if not todos:
|
|
15668
17086
|
return ""
|
|
17087
|
+
# plan step 模式
|
|
17088
|
+
has_plan_steps = any(t.get("category") == "plan_step" for t in todos)
|
|
17089
|
+
if has_plan_steps:
|
|
17090
|
+
completed = [t for t in todos if t.get("category") == "plan_step" and t.get("status") == "completed"]
|
|
17091
|
+
pending = [t for t in todos if t.get("category") == "plan_step" and t.get("status") != "completed"]
|
|
17092
|
+
if not pending:
|
|
17093
|
+
return f"All {len(completed)} plan steps completed. Route to finish. "
|
|
17094
|
+
cur = pending[0]
|
|
17095
|
+
step_idx = int(cur.get("plan_step_index", 0) or 0) + 1
|
|
17096
|
+
total = len(completed) + len(pending)
|
|
17097
|
+
return (
|
|
17098
|
+
f"⚠️ PLAN STEP {step_idx}/{total}: {trim(str(cur.get('content', '') or ''), 200)}. "
|
|
17099
|
+
f"({len(completed)} completed, {len(pending)} remaining) "
|
|
17100
|
+
f"DO NOT finish until all {total} steps are completed. "
|
|
17101
|
+
f"Focus on THIS step. When done, set advance_plan_step=true. "
|
|
17102
|
+
)
|
|
17103
|
+
# 原有逻辑
|
|
15669
17104
|
pending = [t for t in todos if t.get("status") != "completed"]
|
|
15670
17105
|
if not pending:
|
|
15671
17106
|
return "All project tasks completed. Route to finish. "
|
|
@@ -15693,6 +17128,35 @@ class SessionState:
|
|
|
15693
17128
|
"Explorer should only be used for specific file/API lookups, not broad analysis. "
|
|
15694
17129
|
)
|
|
15695
17130
|
project_todo_hint = self._project_todo_hint_for_manager()
|
|
17131
|
+
plan_context = self._plan_steps_context_for_manager()
|
|
17132
|
+
# If no plan steps, give manager autonomous planning ability
|
|
17133
|
+
if not plan_context:
|
|
17134
|
+
plan_context = (
|
|
17135
|
+
"No pre-approved plan. Analyze the goal, break it into logical phases, "
|
|
17136
|
+
"and delegate one phase at a time. Track progress via blackboard. "
|
|
17137
|
+
"Each phase should use the most appropriate agent for that phase's work. "
|
|
17138
|
+
)
|
|
17139
|
+
# Phase-aware independence hint
|
|
17140
|
+
current_phase = self._infer_current_phase_from_blackboard()
|
|
17141
|
+
phase_hint = ""
|
|
17142
|
+
if current_phase:
|
|
17143
|
+
phase_hint = (
|
|
17144
|
+
f"PHASE INDEPENDENCE: Current phase is '{current_phase}'. "
|
|
17145
|
+
"Each task phase has its own expertise and approach. "
|
|
17146
|
+
"Do NOT force implementation patterns from previous phases onto the current one. "
|
|
17147
|
+
"For research/analysis phases: let the agent think freely and use its domain knowledge. "
|
|
17148
|
+
"For implementation phases: focus on concrete code output. "
|
|
17149
|
+
"For test/review phases: focus on verification, not re-implementation. "
|
|
17150
|
+
)
|
|
17151
|
+
failure_brief = self._failure_aware_brief()
|
|
17152
|
+
failure_hint = ""
|
|
17153
|
+
if failure_brief:
|
|
17154
|
+
failure_hint = (
|
|
17155
|
+
"FAILURE CONTEXT (from ledger):\n"
|
|
17156
|
+
f"{trim(failure_brief, 600)}\n"
|
|
17157
|
+
"IMPORTANT: Previous fix attempts FAILED. You MUST change your approach — "
|
|
17158
|
+
"do NOT repeat the same instruction. Include the exact error output in your delegation. "
|
|
17159
|
+
)
|
|
15696
17160
|
return (
|
|
15697
17161
|
"You are Manager in a multi-agent coding system. "
|
|
15698
17162
|
"Read blackboard, delegate one short timeslice via route_to_next_agent. "
|
|
@@ -15701,10 +17165,13 @@ class SessionState:
|
|
|
15701
17165
|
"Role capabilities: "
|
|
15702
17166
|
"Explorer=read-only (bash/read_file/search/blackboard, NO write_file/edit_file); "
|
|
15703
17167
|
"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. "
|
|
17168
|
+
f"{'Reviewer=DEBUG MODE (bash/read_file/write_file/edit_file/finish_task — can fix bugs directly). ' if bool(self.reviewer_debug_mode) else 'Reviewer=read+verify (bash/read_file/finish_task, NO write_file/edit_file). '}"
|
|
17169
|
+
f"{'NEVER delegate file-writing tasks to Explorer. Reviewer is in debug mode and can fix bugs. ' if bool(self.reviewer_debug_mode) else 'NEVER delegate file-writing tasks to Explorer or Reviewer. '}"
|
|
15706
17170
|
f"{coding_hint}"
|
|
15707
17171
|
f"{project_todo_hint}"
|
|
17172
|
+
f"{plan_context}"
|
|
17173
|
+
f"{phase_hint}"
|
|
17174
|
+
f"{failure_hint}"
|
|
15708
17175
|
f"Level={level}, mode={mode}, progress={progress}, "
|
|
15709
17176
|
f"budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
|
|
15710
17177
|
f"objective={trim(str(profile.get('direct_objective','') or ''), 220)}. "
|
|
@@ -15871,7 +17338,25 @@ class SessionState:
|
|
|
15871
17338
|
cycles = int(board.get("manager_cycles", 0) or 0)
|
|
15872
17339
|
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
15873
17340
|
max_budget = max(1, int(getattr(self, "max_agent_rounds", MAX_AGENT_ROUNDS) or MAX_AGENT_ROUNDS))
|
|
17341
|
+
# Hard guard: pending plan steps block finish — redirect to developer instead
|
|
17342
|
+
pending_plan_steps = [
|
|
17343
|
+
t for t in board.get("project_todos", [])
|
|
17344
|
+
if t.get("category") == "plan_step" and t.get("status") != "completed"
|
|
17345
|
+
]
|
|
15874
17346
|
if cycles >= max_budget:
|
|
17347
|
+
if pending_plan_steps:
|
|
17348
|
+
# Budget exhausted but plan steps remain — continue with current step instead of finishing
|
|
17349
|
+
cur = pending_plan_steps[0]
|
|
17350
|
+
self._emit("status", {
|
|
17351
|
+
"summary": f"Max cycles reached but {len(pending_plan_steps)} plan steps pending; continuing."
|
|
17352
|
+
})
|
|
17353
|
+
return {
|
|
17354
|
+
"target": "developer",
|
|
17355
|
+
"instruction": f"Continue with plan step: {trim(str(cur.get('content', '')), 300)}",
|
|
17356
|
+
"reason": f"budget-exhausted-but-plan-steps-pending:{len(pending_plan_steps)}",
|
|
17357
|
+
"source": "fallback",
|
|
17358
|
+
"advance_plan_step": False,
|
|
17359
|
+
}
|
|
15875
17360
|
self._emit("status", {"summary": "Max cycles reached; forcing finish."})
|
|
15876
17361
|
return {
|
|
15877
17362
|
"target": "finish",
|
|
@@ -16147,12 +17632,43 @@ class SessionState:
|
|
|
16147
17632
|
return row
|
|
16148
17633
|
target = str(row.get("target", "") or "").strip().lower()
|
|
16149
17634
|
task_type_low = str(row.get("task_type", "") or "").strip().lower()
|
|
17635
|
+
# 5a: Merge in-memory routes with persisted routes for detection
|
|
17636
|
+
bb_for_routes = self._ensure_blackboard()
|
|
17637
|
+
persisted_routes = bb_for_routes.get("persisted_manager_routes", [])
|
|
17638
|
+
if not isinstance(persisted_routes, list):
|
|
17639
|
+
persisted_routes = []
|
|
17640
|
+
merged_routes = list(persisted_routes) + list(self.manager_routes)
|
|
17641
|
+
# 5b: Delegation repetition detection from failure_ledger
|
|
17642
|
+
fl = bb_for_routes.get("failure_ledger", {})
|
|
17643
|
+
if isinstance(fl, dict):
|
|
17644
|
+
for deleg in (fl.get("repeated_delegations") or []):
|
|
17645
|
+
if (
|
|
17646
|
+
isinstance(deleg, dict)
|
|
17647
|
+
and str(deleg.get("target", "") or "").strip().lower() == target
|
|
17648
|
+
and int(deleg.get("count", 0) or 0) >= 3
|
|
17649
|
+
):
|
|
17650
|
+
alt_targets = [r for r in ("reviewer", "developer", "explorer") if r != target]
|
|
17651
|
+
if len(bb_for_routes.get("code_artifacts", {}) or {}) > 0:
|
|
17652
|
+
row["target"] = "finish"
|
|
17653
|
+
row["instruction"] = (
|
|
17654
|
+
f"Anti-stall: delegation to '{target}' repeated {deleg.get('count')} times with same instruction. "
|
|
17655
|
+
"Forcing finish to break loop."
|
|
17656
|
+
)
|
|
17657
|
+
else:
|
|
17658
|
+
row["target"] = alt_targets[0] if alt_targets else "developer"
|
|
17659
|
+
row["instruction"] = (
|
|
17660
|
+
f"Anti-stall: delegation to '{target}' repeated {deleg.get('count')} times. "
|
|
17661
|
+
f"Switching to {row['target']} with fresh approach."
|
|
17662
|
+
)
|
|
17663
|
+
row["reason"] = f"{row.get('reason', '')}|anti-stall-repeated-delegation"
|
|
17664
|
+
row["source"] = "anti-stall"
|
|
17665
|
+
return row
|
|
16150
17666
|
if task_type_low in ("simple_code", "engineering") and target == "explorer":
|
|
16151
|
-
board =
|
|
17667
|
+
board = bb_for_routes
|
|
16152
17668
|
progress = self._manager_progress_state(board)
|
|
16153
17669
|
if progress in ("initializing", "in_progress"):
|
|
16154
17670
|
explorer_count = sum(
|
|
16155
|
-
1 for x in
|
|
17671
|
+
1 for x in merged_routes[-8:]
|
|
16156
17672
|
if str(x.get("target", "") or "").strip().lower() == "explorer"
|
|
16157
17673
|
)
|
|
16158
17674
|
if explorer_count >= 2:
|
|
@@ -16166,9 +17682,9 @@ class SessionState:
|
|
|
16166
17682
|
return row
|
|
16167
17683
|
if target not in AGENT_ROLES:
|
|
16168
17684
|
return row
|
|
16169
|
-
recent = [str(x.get("target", "") or "").strip().lower() for x in
|
|
17685
|
+
recent = [str(x.get("target", "") or "").strip().lower() for x in merged_routes[-4:]]
|
|
16170
17686
|
if len(recent) >= 2 and recent[-1] == target and recent[-2] == target:
|
|
16171
|
-
board =
|
|
17687
|
+
board = bb_for_routes
|
|
16172
17688
|
low_reason = str(row.get("reason", "") or "").strip().lower()
|
|
16173
17689
|
if "summary" in low_reason and len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16174
17690
|
row["target"] = "finish"
|
|
@@ -16191,7 +17707,7 @@ class SessionState:
|
|
|
16191
17707
|
row["source"] = "anti-stall"
|
|
16192
17708
|
return row
|
|
16193
17709
|
if len(recent) == 4 and recent[0] == recent[2] and recent[1] == recent[3] and recent[0] != recent[1]:
|
|
16194
|
-
board =
|
|
17710
|
+
board = bb_for_routes
|
|
16195
17711
|
if len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16196
17712
|
row["target"] = "finish"
|
|
16197
17713
|
row["instruction"] = "Oscillation detected with existing outputs; finish now."
|
|
@@ -16382,7 +17898,17 @@ class SessionState:
|
|
|
16382
17898
|
target = assigned_expert
|
|
16383
17899
|
else:
|
|
16384
17900
|
if self._manager_has_error_log(board):
|
|
16385
|
-
|
|
17901
|
+
# Error exists: activate reviewer debug mode and route to reviewer
|
|
17902
|
+
error_logs = board.get("execution_logs", []) if isinstance(board.get("execution_logs"), list) else []
|
|
17903
|
+
error_summary = ""
|
|
17904
|
+
for log in reversed(error_logs[-5:]):
|
|
17905
|
+
if isinstance(log, dict) and str(log.get("category", "")).lower() in ("compilation", "runtime", "test", "lint", "build"):
|
|
17906
|
+
error_summary = trim(str(log.get("error", log.get("output", "")) or ""), 500)
|
|
17907
|
+
break
|
|
17908
|
+
if not error_summary:
|
|
17909
|
+
error_summary = "Error detected in execution logs. Check blackboard for details."
|
|
17910
|
+
self._activate_reviewer_debug_mode(error_summary)
|
|
17911
|
+
target = "reviewer"
|
|
16386
17912
|
elif len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16387
17913
|
target = "reviewer"
|
|
16388
17914
|
elif len(board.get("research_notes", []) or []) > 0:
|
|
@@ -16562,6 +18088,7 @@ class SessionState:
|
|
|
16562
18088
|
"requires_user_confirmation": bool(args.get("requires_user_confirmation", False)),
|
|
16563
18089
|
"is_mandatory": _to_bool_like(args.get("is_mandatory", False), default=False),
|
|
16564
18090
|
"executor_mode": _to_bool_like(args.get("executor_mode", False), default=False),
|
|
18091
|
+
"advance_plan_step": _to_bool_like(args.get("advance_plan_step", False), default=False),
|
|
16565
18092
|
"round_budget": args.get("round_budget", 0),
|
|
16566
18093
|
"reason": trim(str(text or "").strip(), 600),
|
|
16567
18094
|
"source": "tool",
|
|
@@ -16858,6 +18385,29 @@ class SessionState:
|
|
|
16858
18385
|
active_profile = self._ensure_blackboard_task_profile(board)
|
|
16859
18386
|
target = str(route.get("target", "") or "").strip().lower()
|
|
16860
18387
|
instruction = trim(str(route.get("instruction", "") or "").strip(), 1200)
|
|
18388
|
+
# Anti-stall: detect explorer consecutive delegation loop
|
|
18389
|
+
if target == "explorer":
|
|
18390
|
+
inst_hash = str(hash(instruction[:200]))
|
|
18391
|
+
if inst_hash == self.last_explorer_instruction_hash:
|
|
18392
|
+
self.explorer_consecutive_delegations += 1
|
|
18393
|
+
else:
|
|
18394
|
+
self.explorer_consecutive_delegations = 1
|
|
18395
|
+
self.last_explorer_instruction_hash = inst_hash
|
|
18396
|
+
if self.explorer_consecutive_delegations >= EXPLORER_STALL_THRESHOLD:
|
|
18397
|
+
self._emit("status", {
|
|
18398
|
+
"summary": f"explorer stall detected ({self.explorer_consecutive_delegations} consecutive); switching to developer"
|
|
18399
|
+
})
|
|
18400
|
+
target = "developer"
|
|
18401
|
+
instruction = f"Explorer is stalled. Take over: {instruction}"
|
|
18402
|
+
self.explorer_consecutive_delegations = 0
|
|
18403
|
+
else:
|
|
18404
|
+
self.explorer_consecutive_delegations = 0
|
|
18405
|
+
self.last_explorer_instruction_hash = ""
|
|
18406
|
+
# Reviewer debug mode timeout check
|
|
18407
|
+
if target == "reviewer" and bool(self.reviewer_debug_mode):
|
|
18408
|
+
if self._check_reviewer_debug_mode_timeout():
|
|
18409
|
+
target = "developer"
|
|
18410
|
+
instruction = f"Reviewer debug mode timed out. Developer take over: {instruction}"
|
|
16861
18411
|
try:
|
|
16862
18412
|
task_level = int(route.get("task_level", active_profile.get("task_level", self.runtime_task_level or 3)) or 3)
|
|
16863
18413
|
except Exception:
|
|
@@ -16925,8 +18475,27 @@ class SessionState:
|
|
|
16925
18475
|
"round_budget": int(round_budget),
|
|
16926
18476
|
"remaining_rounds": int(remaining_rounds),
|
|
16927
18477
|
}
|
|
18478
|
+
# advance_plan_step: 当前 plan step 完成,推进到下一步
|
|
18479
|
+
if _to_bool_like(route.get("advance_plan_step", False), default=False):
|
|
18480
|
+
self._advance_plan_step(
|
|
18481
|
+
evidence=trim(str(route.get("instruction", "") or ""), 200),
|
|
18482
|
+
actor=str(route.get("target", "developer") or "developer"),
|
|
18483
|
+
)
|
|
16928
18484
|
self.manager_routes.append(route_row)
|
|
16929
18485
|
self.manager_routes = self.manager_routes[-240:]
|
|
18486
|
+
# Failure ledger: persist route and record delegation
|
|
18487
|
+
_pmr = board.get("persisted_manager_routes", [])
|
|
18488
|
+
if not isinstance(_pmr, list):
|
|
18489
|
+
_pmr = []
|
|
18490
|
+
_pmr.append({
|
|
18491
|
+
"target": target,
|
|
18492
|
+
"instruction_hash": hashlib.sha1(str(instruction or "").encode("utf-8")).hexdigest()[:12],
|
|
18493
|
+
"round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
18494
|
+
"ts": float(now_ts()),
|
|
18495
|
+
})
|
|
18496
|
+
board["persisted_manager_routes"] = _pmr[-PERSISTED_ROUTES_MAX:]
|
|
18497
|
+
self.blackboard = board
|
|
18498
|
+
self._ledger_record_delegation(target, instruction)
|
|
16930
18499
|
profile = self._ensure_blackboard_task_profile(board)
|
|
16931
18500
|
profile["task_level"] = int(task_level)
|
|
16932
18501
|
profile["execution_mode"] = execution_mode
|
|
@@ -17089,7 +18658,23 @@ class SessionState:
|
|
|
17089
18658
|
role_capability_note = {
|
|
17090
18659
|
"explorer": "YOUR TOOLS: read-only (bash/read_file/search/blackboard). You CANNOT write_file or edit_file.",
|
|
17091
18660
|
"developer": "YOUR TOOLS: all tools available (write_file/edit_file/bash/read_file/etc).",
|
|
17092
|
-
"reviewer":
|
|
18661
|
+
"reviewer": (
|
|
18662
|
+
"YOUR TOOLS: read+verify+DEBUG (bash/read_file/write_file/edit_file/finish_task). "
|
|
18663
|
+
"You CAN directly edit code to fix bugs when in debug mode. "
|
|
18664
|
+
"DEBUG METHODOLOGY: 1) read_file to see the exact problematic line, "
|
|
18665
|
+
"2) analyze the error output character-by-character, "
|
|
18666
|
+
"3) edit_file to fix the root cause directly, "
|
|
18667
|
+
"4) bash to verify the fix compiles/runs, "
|
|
18668
|
+
"5) finish_task when the error is resolved. "
|
|
18669
|
+
) if bool(self.reviewer_debug_mode) else (
|
|
18670
|
+
"YOUR TOOLS: read+verify (bash/read_file/finish_task). You CANNOT write_file or edit_file. "
|
|
18671
|
+
"When you find errors: 1) read_file to see the exact problematic line, "
|
|
18672
|
+
"2) analyze the error output character-by-character, "
|
|
18673
|
+
"3) send ask_colleague(to='developer', intent='fix_request') with STRUCTURED content: "
|
|
18674
|
+
"ERROR: <exact error output>, FILE: <path>, LINE: <number>, "
|
|
18675
|
+
"ROOT_CAUSE: <analysis>, CATEGORY: <compilation|runtime|test|lint|build|deploy>, "
|
|
18676
|
+
"FIX: change '<old>' to '<new>' OR <action to take>. "
|
|
18677
|
+
),
|
|
17093
18678
|
}.get(role_key, "")
|
|
17094
18679
|
if role_key == "explorer":
|
|
17095
18680
|
tool_examples = "bash/read_file/read_from_blackboard"
|
|
@@ -17120,6 +18705,16 @@ class SessionState:
|
|
|
17120
18705
|
"use ask_colleague immediately with explicit intent and concise payload; "
|
|
17121
18706
|
"do not wait for another manager cycle."
|
|
17122
18707
|
)
|
|
18708
|
+
compile_error_ctx = self._recent_compile_error_context(max_chars=800)
|
|
18709
|
+
error_section = ""
|
|
18710
|
+
if compile_error_ctx:
|
|
18711
|
+
error_section = (
|
|
18712
|
+
"<compile-error-context>\n"
|
|
18713
|
+
f"{compile_error_ctx}\n"
|
|
18714
|
+
"CRITICAL: Read the EXACT error message above. Fix the SPECIFIC syntax/character issue. "
|
|
18715
|
+
"Do NOT repeat previous failed fixes.\n"
|
|
18716
|
+
"</compile-error-context>\n"
|
|
18717
|
+
)
|
|
17123
18718
|
board_md = self._blackboard_read_state_markdown(max_items=5)
|
|
17124
18719
|
payload = (
|
|
17125
18720
|
"<manager-delegate>\n"
|
|
@@ -17132,6 +18727,7 @@ class SessionState:
|
|
|
17132
18727
|
f"{executor_note}\n"
|
|
17133
18728
|
f"{collaboration_note}\n"
|
|
17134
18729
|
f"{role_capability_note}\n"
|
|
18730
|
+
f"{error_section}"
|
|
17135
18731
|
"</manager-delegate>\n"
|
|
17136
18732
|
"<blackboard-state>\n"
|
|
17137
18733
|
f"{trim(board_md, 6000)}\n"
|
|
@@ -17660,10 +19256,13 @@ class SessionState:
|
|
|
17660
19256
|
row["agent_role"] = role_key
|
|
17661
19257
|
if "ts" not in row:
|
|
17662
19258
|
row["ts"] = now_ts()
|
|
17663
|
-
# Write to unified agent_messages
|
|
19259
|
+
# Write to unified agent_messages with tier-aware trim
|
|
17664
19260
|
self.agent_messages.append(row)
|
|
17665
|
-
|
|
17666
|
-
|
|
19261
|
+
tier = self._context_compression_tier()
|
|
19262
|
+
am_limit = self._tier_agent_context_limits(tier)["agent_messages"]
|
|
19263
|
+
overflow = int(am_limit * 1.5) # trigger trim at 1.5x limit
|
|
19264
|
+
if len(self.agent_messages) > overflow:
|
|
19265
|
+
self.agent_messages = self.agent_messages[-am_limit:]
|
|
17667
19266
|
if mirror_to_global:
|
|
17668
19267
|
mirror = dict(row)
|
|
17669
19268
|
if "tool_calls" in mirror and isinstance(mirror.get("tool_calls"), list):
|
|
@@ -17691,10 +19290,14 @@ class SessionState:
|
|
|
17691
19290
|
if "ts" not in row:
|
|
17692
19291
|
row["ts"] = now_ts()
|
|
17693
19292
|
self.manager_context.append(row)
|
|
17694
|
-
|
|
19293
|
+
tier = self._context_compression_tier()
|
|
19294
|
+
mgr_limit = self._tier_agent_context_limits(tier)["manager_context"]
|
|
19295
|
+
if len(self.manager_context) > int(mgr_limit * 1.5):
|
|
19296
|
+
self.manager_context = self.manager_context[-mgr_limit:]
|
|
17695
19297
|
self.agent_messages.append(row)
|
|
17696
|
-
|
|
17697
|
-
|
|
19298
|
+
am_limit = self._tier_agent_context_limits(tier)["agent_messages"]
|
|
19299
|
+
if len(self.agent_messages) > int(am_limit * 1.5):
|
|
19300
|
+
self.agent_messages = self.agent_messages[-am_limit:]
|
|
17698
19301
|
|
|
17699
19302
|
def _agent_display_name(self, role: str) -> str:
|
|
17700
19303
|
return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
|
|
@@ -17849,12 +19452,41 @@ class SessionState:
|
|
|
17849
19452
|
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
17850
19453
|
"Keep outputs concise and action-oriented. "
|
|
17851
19454
|
"Use load_skill for detailed guidance (multi-agent-guide, code-review-checklist, finish-protocol). "
|
|
19455
|
+
f"{_detect_os_shell_instruction()} "
|
|
17852
19456
|
f"{model_language_instruction(self.ui_language)} "
|
|
17853
19457
|
)
|
|
17854
19458
|
if role_key == "explorer":
|
|
17855
19459
|
return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
|
|
17856
19460
|
if role_key == "reviewer":
|
|
17857
|
-
|
|
19461
|
+
if bool(self.reviewer_debug_mode):
|
|
19462
|
+
debug_ctx = trim(str(self.reviewer_debug_context or ""), 500)
|
|
19463
|
+
return base + (
|
|
19464
|
+
"Role: DEBUG MODE ACTIVE — independent bug fix process. "
|
|
19465
|
+
"You have FULL write access: write_file, edit_file, bash, read_file, finish_task. "
|
|
19466
|
+
"You CAN and SHOULD directly edit code to fix bugs. "
|
|
19467
|
+
"Be methodical and calm: "
|
|
19468
|
+
"1) Read the error context carefully. "
|
|
19469
|
+
"2) Locate the root cause in the source file. "
|
|
19470
|
+
"3) Fix it with edit_file (precise, minimal changes). "
|
|
19471
|
+
"4) Verify the fix compiles/runs with bash. "
|
|
19472
|
+
"5) Call finish_task when the error is resolved. "
|
|
19473
|
+
f"DEBUG CONTEXT: {debug_ctx} "
|
|
19474
|
+
)
|
|
19475
|
+
return base + (
|
|
19476
|
+
"Role: verify output, run checks, issue pass/fix decisions. Write review_feedback to blackboard. "
|
|
19477
|
+
"DEBUG METHODOLOGY when any error occurs (compilation, runtime, test, lint, build, deploy): "
|
|
19478
|
+
"1) Read the EXACT error message — identify the file, line number, and error type. "
|
|
19479
|
+
"2) Read the source file at that exact line — examine what the error points to. "
|
|
19480
|
+
"3) Identify the ROOT CAUSE — match the error to a specific rule violation. "
|
|
19481
|
+
"For runtime errors: identify the traceback, exception type, and the triggering line. "
|
|
19482
|
+
"For test failures: identify which test failed, the assertion, expected vs actual. "
|
|
19483
|
+
"For lint/type errors: identify the rule violation and exact location. "
|
|
19484
|
+
"4) When sending fix_request via ask_colleague, you MUST include: "
|
|
19485
|
+
"the exact error output, the file and line number, the root cause analysis, "
|
|
19486
|
+
"the error category (compilation/runtime/test/lint/build/deploy), "
|
|
19487
|
+
"and the precise fix (what to change FROM and TO). "
|
|
19488
|
+
"NEVER send vague fix requests without verifying the actual cause. "
|
|
19489
|
+
)
|
|
17858
19490
|
return base + "Role: implement code changes, execute tools, record progress to blackboard. "
|
|
17859
19491
|
|
|
17860
19492
|
def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
|
|
@@ -18582,11 +20214,9 @@ class SessionState:
|
|
|
18582
20214
|
self._emit("status", {"summary": summary})
|
|
18583
20215
|
out = f"{out}\n{summary}"
|
|
18584
20216
|
after_text = try_read_text(fp, max_bytes=CODE_PREVIEW_STAGE_MAX_BYTES) or ""
|
|
18585
|
-
diff = make_unified_diff(rel, before_text, after_text)
|
|
20217
|
+
diff, added, deleted = make_unified_diff(rel, before_text, after_text)
|
|
18586
20218
|
numbered = make_numbered_diff(before_text, after_text)
|
|
18587
20219
|
numbered_text = render_numbered_diff_text(numbered)
|
|
18588
|
-
added = sum(1 for ln in diff.splitlines() if ln.startswith("+") and not ln.startswith("+++"))
|
|
18589
|
-
deleted = sum(1 for ln in diff.splitlines() if ln.startswith("-") and not ln.startswith("---"))
|
|
18590
20220
|
code_stage = self._record_code_preview_stage(
|
|
18591
20221
|
rel_path=rel,
|
|
18592
20222
|
before_text=before_text,
|
|
@@ -18635,11 +20265,9 @@ class SessionState:
|
|
|
18635
20265
|
self._emit("status", {"summary": summary})
|
|
18636
20266
|
out = f"{out}\n{summary}"
|
|
18637
20267
|
after_text = try_read_text(fp, max_bytes=CODE_PREVIEW_STAGE_MAX_BYTES) or ""
|
|
18638
|
-
diff = make_unified_diff(rel, before_text, after_text)
|
|
20268
|
+
diff, added, deleted = make_unified_diff(rel, before_text, after_text)
|
|
18639
20269
|
numbered = make_numbered_diff(before_text, after_text)
|
|
18640
20270
|
numbered_text = render_numbered_diff_text(numbered)
|
|
18641
|
-
added = sum(1 for ln in diff.splitlines() if ln.startswith("+") and not ln.startswith("+++"))
|
|
18642
|
-
deleted = sum(1 for ln in diff.splitlines() if ln.startswith("-") and not ln.startswith("---"))
|
|
18643
20271
|
code_stage = self._record_code_preview_stage(
|
|
18644
20272
|
rel_path=rel,
|
|
18645
20273
|
before_text=before_text,
|
|
@@ -18670,6 +20298,20 @@ class SessionState:
|
|
|
18670
20298
|
)
|
|
18671
20299
|
return out
|
|
18672
20300
|
if name == "TodoWrite":
|
|
20301
|
+
# Protect plan_step todos: worker TodoWrite creates sub-items, not replacements
|
|
20302
|
+
bb = self._ensure_blackboard()
|
|
20303
|
+
has_plan_steps = any(
|
|
20304
|
+
t.get("category") == "plan_step"
|
|
20305
|
+
for t in bb.get("project_todos", [])
|
|
20306
|
+
)
|
|
20307
|
+
if has_plan_steps:
|
|
20308
|
+
# Tag worker items as sub-tasks so _sync_todos_from_blackboard won't conflict
|
|
20309
|
+
items = args.get("items", [])
|
|
20310
|
+
if isinstance(items, list):
|
|
20311
|
+
for item in items:
|
|
20312
|
+
if isinstance(item, dict) and not item.get("key", "").startswith("bb:"):
|
|
20313
|
+
item["owner"] = str(role_key or "developer")
|
|
20314
|
+
return self.todo.update(items)
|
|
18673
20315
|
return self.todo.update(args["items"])
|
|
18674
20316
|
if name == "TodoWriteRescue":
|
|
18675
20317
|
return self._todo_write_rescue(args)
|
|
@@ -18710,6 +20352,9 @@ class SessionState:
|
|
|
18710
20352
|
todo_mark = self.todo.complete_all_open(summary)
|
|
18711
20353
|
else:
|
|
18712
20354
|
todo_mark = self.todo.complete_active(summary)
|
|
20355
|
+
# Deactivate reviewer debug mode on finish
|
|
20356
|
+
if bool(self.reviewer_debug_mode) and role_key == "reviewer":
|
|
20357
|
+
self._deactivate_reviewer_debug_mode("reviewer finished")
|
|
18713
20358
|
updated = int(todo_mark.get("updated", 0) or 0)
|
|
18714
20359
|
if updated > 0:
|
|
18715
20360
|
self._emit(
|
|
@@ -18822,6 +20467,25 @@ class SessionState:
|
|
|
18822
20467
|
return "Error: ask_colleague.intent is required"
|
|
18823
20468
|
if not content:
|
|
18824
20469
|
return "Error: ask_colleague.content is required"
|
|
20470
|
+
if intent == "fix_request":
|
|
20471
|
+
required_markers = ["ERROR:", "ROOT_CAUSE:", "FIX:"]
|
|
20472
|
+
soft_markers = ["FILE:", "LINE:", "CATEGORY:"]
|
|
20473
|
+
missing = [m for m in required_markers if m not in content.upper()]
|
|
20474
|
+
missing_soft = [m for m in soft_markers if m not in content.upper()]
|
|
20475
|
+
if missing or missing_soft:
|
|
20476
|
+
hint_parts = []
|
|
20477
|
+
if missing:
|
|
20478
|
+
hint_parts.append(f"Missing required: {', '.join(missing)}")
|
|
20479
|
+
if missing_soft:
|
|
20480
|
+
hint_parts.append(f"Recommended: {', '.join(missing_soft)}")
|
|
20481
|
+
content = (
|
|
20482
|
+
f"{content}\n\n"
|
|
20483
|
+
"[SYSTEM NOTE: fix_request should include structured fields: "
|
|
20484
|
+
"ERROR: <exact error>, FILE: <path>, LINE: <number>, "
|
|
20485
|
+
"ROOT_CAUSE: <analysis>, CATEGORY: <compilation|runtime|test|lint|build|deploy>, "
|
|
20486
|
+
"FIX: <specific change>. "
|
|
20487
|
+
f"{'; '.join(hint_parts)}]"
|
|
20488
|
+
)
|
|
18825
20489
|
env = self._agent_send_bus(from_role, to_role, intent, content)
|
|
18826
20490
|
return (
|
|
18827
20491
|
f"agent_bus sent ({env.get('from')} -> {env.get('to')}, "
|
|
@@ -19033,6 +20697,8 @@ class SessionState:
|
|
|
19033
20697
|
"</live-user-adjustment>"
|
|
19034
20698
|
)
|
|
19035
20699
|
self.messages.append({"role": "user", "content": payload, "ts": now_ts()})
|
|
20700
|
+
# Merge user feedback with plan direction
|
|
20701
|
+
self._merge_user_feedback_with_plan(content)
|
|
19036
20702
|
self.runtime_reclassify_goal = trim(content, 4000)
|
|
19037
20703
|
self.runtime_reclassify_required = True
|
|
19038
20704
|
self.runtime_goal_reset_pending = True
|
|
@@ -19066,6 +20732,104 @@ class SessionState:
|
|
|
19066
20732
|
)
|
|
19067
20733
|
return len(injected)
|
|
19068
20734
|
|
|
20735
|
+
def _merge_user_feedback_with_plan(self, user_text: str):
|
|
20736
|
+
"""When user provides feedback during execution, inject plan-aware merge note into manager context."""
|
|
20737
|
+
bb = self._ensure_blackboard()
|
|
20738
|
+
plan = bb.get("plan", {})
|
|
20739
|
+
if not isinstance(plan, dict):
|
|
20740
|
+
return
|
|
20741
|
+
current_step = None
|
|
20742
|
+
for t in bb.get("project_todos", []):
|
|
20743
|
+
if t.get("category") == "plan_step" and t.get("status") == "in_progress":
|
|
20744
|
+
current_step = t
|
|
20745
|
+
break
|
|
20746
|
+
step_desc = trim(str(current_step.get("content", "") if current_step else "none"), 200)
|
|
20747
|
+
is_plan_executing = plan.get("phase") == "executing"
|
|
20748
|
+
if is_plan_executing:
|
|
20749
|
+
merge_note = (
|
|
20750
|
+
f"<user-feedback-merge>\n"
|
|
20751
|
+
f"User provided new input during plan execution: {trim(user_text, 500)}\n"
|
|
20752
|
+
f"Current plan step: {step_desc}\n"
|
|
20753
|
+
f"Re-evaluate: Does this feedback change the current step's approach? "
|
|
20754
|
+
f"If yes, adjust delegation accordingly. If it's a new requirement, "
|
|
20755
|
+
f"integrate it into the current or next step. Do NOT restart from scratch.\n"
|
|
20756
|
+
f"</user-feedback-merge>"
|
|
20757
|
+
)
|
|
20758
|
+
else:
|
|
20759
|
+
merge_note = (
|
|
20760
|
+
f"<user-feedback-merge>\n"
|
|
20761
|
+
f"User provided new input: {trim(user_text, 500)}\n"
|
|
20762
|
+
f"Integrate this feedback with current work direction. "
|
|
20763
|
+
f"Adjust approach if needed but maintain progress.\n"
|
|
20764
|
+
f"</user-feedback-merge>"
|
|
20765
|
+
)
|
|
20766
|
+
if self._is_multi_agent_mode():
|
|
20767
|
+
self._append_manager_context({
|
|
20768
|
+
"role": "system",
|
|
20769
|
+
"content": merge_note,
|
|
20770
|
+
"ts": now_ts(),
|
|
20771
|
+
"agent_role": "manager",
|
|
20772
|
+
})
|
|
20773
|
+
|
|
20774
|
+
def _is_restart_scenario(self) -> bool:
|
|
20775
|
+
"""Check if current state is a restart after finished/aborted task."""
|
|
20776
|
+
bb = self._ensure_blackboard()
|
|
20777
|
+
status = str(bb.get("status", "") or "").upper()
|
|
20778
|
+
approval = bb.get("approval", {}) if isinstance(bb.get("approval"), dict) else {}
|
|
20779
|
+
return status in ("COMPLETED", "ABORTED") or bool(approval.get("approved", False))
|
|
20780
|
+
|
|
20781
|
+
def _fuse_restart_intent(self, user_text: str):
|
|
20782
|
+
"""Fuse user intent with plan/context intent on restart.
|
|
20783
|
+
Priority: 1. user intent, 2. plan intent, 3. context intent."""
|
|
20784
|
+
bb = self._ensure_blackboard()
|
|
20785
|
+
plan = bb.get("plan", {}) if isinstance(bb.get("plan"), dict) else {}
|
|
20786
|
+
original_goal = trim(str(bb.get("original_goal", "") or ""), 500)
|
|
20787
|
+
plan_steps = plan.get("steps", []) if isinstance(plan.get("steps"), list) else []
|
|
20788
|
+
# Detect pure continuation
|
|
20789
|
+
_CONTINUE_PHRASES = {
|
|
20790
|
+
"继续", "continue", "go on", "接着", "接续", "keep going", "proceed",
|
|
20791
|
+
"继续执行", "接着做", "go ahead", "next", "下一步",
|
|
20792
|
+
}
|
|
20793
|
+
is_continuation = str(user_text or "").strip().lower().rstrip("!.。!") in _CONTINUE_PHRASES
|
|
20794
|
+
if is_continuation and plan_steps:
|
|
20795
|
+
# Pure continuation — inherit plan intent fully
|
|
20796
|
+
pending = [s for i, s in enumerate(plan_steps) if i >= int(bb.get("plan_step_cursor", 0) or 0)]
|
|
20797
|
+
if pending:
|
|
20798
|
+
fused = (
|
|
20799
|
+
f"<intent-fusion type='continuation'>\n"
|
|
20800
|
+
f"User said: {trim(user_text, 100)}\n"
|
|
20801
|
+
f"Continuing approved plan. Original goal: {original_goal}\n"
|
|
20802
|
+
f"Remaining steps: {', '.join(trim(str(s), 80) for s in pending[:5])}\n"
|
|
20803
|
+
f"Resume from where we left off. Do NOT restart.\n"
|
|
20804
|
+
f"</intent-fusion>"
|
|
20805
|
+
)
|
|
20806
|
+
self.messages.append({"role": "user", "content": fused, "ts": now_ts()})
|
|
20807
|
+
# Don't reset blackboard — continue
|
|
20808
|
+
self.runtime_goal_reset_pending = False
|
|
20809
|
+
return
|
|
20810
|
+
if is_continuation and original_goal:
|
|
20811
|
+
# Continuation without plan — inherit context intent
|
|
20812
|
+
fused = (
|
|
20813
|
+
f"<intent-fusion type='context-continuation'>\n"
|
|
20814
|
+
f"User said: {trim(user_text, 100)}\n"
|
|
20815
|
+
f"Continue previous work. Original goal: {original_goal}\n"
|
|
20816
|
+
f"Resume from current state. Do NOT restart.\n"
|
|
20817
|
+
f"</intent-fusion>"
|
|
20818
|
+
)
|
|
20819
|
+
self.messages.append({"role": "user", "content": fused, "ts": now_ts()})
|
|
20820
|
+
self.runtime_goal_reset_pending = False
|
|
20821
|
+
return
|
|
20822
|
+
# New instruction — fuse with context but user intent takes priority
|
|
20823
|
+
if original_goal:
|
|
20824
|
+
fused = (
|
|
20825
|
+
f"<intent-fusion type='new-with-context'>\n"
|
|
20826
|
+
f"New user instruction: {trim(user_text, 500)}\n"
|
|
20827
|
+
f"Previous context: {original_goal}\n"
|
|
20828
|
+
f"User's new instruction takes priority. Use previous context for background only.\n"
|
|
20829
|
+
f"</intent-fusion>"
|
|
20830
|
+
)
|
|
20831
|
+
self.messages.append({"role": "user", "content": fused, "ts": now_ts()})
|
|
20832
|
+
|
|
19069
20833
|
def submit_user_message(self, content: str):
|
|
19070
20834
|
start_worker = False
|
|
19071
20835
|
dropped_stale_inputs = 0
|
|
@@ -19076,12 +20840,37 @@ class SessionState:
|
|
|
19076
20840
|
else:
|
|
19077
20841
|
if self.pending_user_inputs:
|
|
19078
20842
|
dropped_stale_inputs = len(self.pending_user_inputs)
|
|
20843
|
+
# Preserve plan state if awaiting user choice
|
|
20844
|
+
_awaiting_plan_choice = bool(
|
|
20845
|
+
self.runtime_plan_proposal
|
|
20846
|
+
and not self.runtime_plan_approved
|
|
20847
|
+
)
|
|
19079
20848
|
removed_runtime_hints = self._reset_runtime_state_locked(purge_runtime_hints=True)
|
|
20849
|
+
if _awaiting_plan_choice:
|
|
20850
|
+
# Restore plan proposal so choice can be parsed
|
|
20851
|
+
self.runtime_plan_mode_needed = True
|
|
19080
20852
|
self.run_generation = int(self.run_generation) + 1
|
|
19081
20853
|
clean_goal = trim(str(content or "").strip(), 4000)
|
|
19082
20854
|
self.messages.append({"role": "user", "content": content, "ts": now_ts()})
|
|
20855
|
+
# Parse plan choice immediately after clean_goal is available
|
|
20856
|
+
if _awaiting_plan_choice:
|
|
20857
|
+
choice = self._parse_plan_choice(clean_goal, self.runtime_plan_proposal)
|
|
20858
|
+
if choice:
|
|
20859
|
+
self.runtime_plan_choice = choice
|
|
20860
|
+
self.runtime_plan_approved = True
|
|
20861
|
+
self.runtime_plan_mode_needed = False
|
|
20862
|
+
self._inject_plan_into_context(choice)
|
|
20863
|
+
self.runtime_reclassify_required = False
|
|
20864
|
+
self.runtime_goal_reset_pending = False
|
|
20865
|
+
# Restart intent fusion: merge user/plan/context intents
|
|
20866
|
+
if self._is_restart_scenario():
|
|
20867
|
+
self._fuse_restart_intent(clean_goal)
|
|
19083
20868
|
self.runtime_reclassify_goal = clean_goal
|
|
19084
|
-
|
|
20869
|
+
# Skip reclassification for plan choice responses — preserve complexity
|
|
20870
|
+
if _awaiting_plan_choice or self._is_plan_choice_response(clean_goal):
|
|
20871
|
+
self.runtime_reclassify_required = False
|
|
20872
|
+
else:
|
|
20873
|
+
self.runtime_reclassify_required = True
|
|
19085
20874
|
self.runtime_goal_reset_pending = True
|
|
19086
20875
|
self.running = True
|
|
19087
20876
|
self.current_phase = "starting"
|
|
@@ -19265,7 +21054,7 @@ class SessionState:
|
|
|
19265
21054
|
for tc in tool_calls
|
|
19266
21055
|
]
|
|
19267
21056
|
self._append_agent_context_message(role_key, assistant, mirror_to_global=True)
|
|
19268
|
-
if text.strip() or thinking_text:
|
|
21057
|
+
if (text.strip() or thinking_text) and not tool_calls:
|
|
19269
21058
|
emit_text = text if text.strip() else "[thinking-only output]"
|
|
19270
21059
|
self._emit_agent_message(role_key, emit_text, summary=f"{self._agent_display_name(role_key)} response")
|
|
19271
21060
|
if not tool_calls:
|
|
@@ -19364,6 +21153,25 @@ class SessionState:
|
|
|
19364
21153
|
}
|
|
19365
21154
|
# Atomic blackboard sync: update shared state immediately after each tool result.
|
|
19366
21155
|
self._blackboard_update_from_tool_result(role_key, item)
|
|
21156
|
+
# Failure ledger: record tool call and detect errors (unified)
|
|
21157
|
+
self._ledger_record_tool_call(name, args if isinstance(args, dict) else {})
|
|
21158
|
+
self._process_tool_result_errors(name, args if isinstance(args, dict) else {}, output, item["ok"], role_key)
|
|
21159
|
+
# Failure ledger: record edit_file as potential fix attempt if errors exist
|
|
21160
|
+
if name in ("edit_file", "write_file") and isinstance(args, dict):
|
|
21161
|
+
fl = self._ensure_blackboard().get("failure_ledger", {})
|
|
21162
|
+
all_errors = fl.get("errors", []) if isinstance(fl, dict) else []
|
|
21163
|
+
edit_path = str(args.get("path", "") or "").strip()
|
|
21164
|
+
if all_errors and edit_path:
|
|
21165
|
+
matching = [e for e in all_errors if edit_path.endswith(str(e.get("file", "") or "")) or str(e.get("file", "") or "").endswith(edit_path)]
|
|
21166
|
+
if matching:
|
|
21167
|
+
fix_desc = trim(str(args.get("new_text", "") or args.get("content", "") or ""), 300)
|
|
21168
|
+
self._ledger_record_fix_attempt(
|
|
21169
|
+
edit_path,
|
|
21170
|
+
trim(str(matching[-1].get("error_msg", "") or ""), 300),
|
|
21171
|
+
fix_desc,
|
|
21172
|
+
"pending",
|
|
21173
|
+
role_key,
|
|
21174
|
+
)
|
|
19367
21175
|
item["bb_applied"] = True
|
|
19368
21176
|
tool_results.append(item)
|
|
19369
21177
|
if name in {"finish_task", "finish_current_task", "mark_done"} and (not str(output).startswith("Error:")):
|
|
@@ -19552,6 +21360,9 @@ class SessionState:
|
|
|
19552
21360
|
self._emit("status", {"summary": "run interrupted"})
|
|
19553
21361
|
break
|
|
19554
21362
|
self._apply_auto_compact_if_needed("auto:multi-sync")
|
|
21363
|
+
# Periodic checkpoint in multi-agent sync loop
|
|
21364
|
+
if rounds_used % CHECKPOINT_INTERVAL_ROUNDS == 0:
|
|
21365
|
+
self._maybe_create_checkpoint()
|
|
19555
21366
|
with self.lock:
|
|
19556
21367
|
self.agent_round_index = int(self.agent_round_index) + 1
|
|
19557
21368
|
self.current_phase = "manager:dispatch"
|
|
@@ -19894,6 +21705,450 @@ class SessionState:
|
|
|
19894
21705
|
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19895
21706
|
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
19896
21707
|
|
|
21708
|
+
# ── Plan Mode Methods ──
|
|
21709
|
+
|
|
21710
|
+
def _plan_mode_worker(self, pinned_selection: str):
|
|
21711
|
+
"""Plan mode: explorer 调研 → manager 综合 → emit 方案 → 等待用户选择"""
|
|
21712
|
+
# Phase 1: Explorer 调研
|
|
21713
|
+
self._emit("status", {"summary": "plan-mode: research phase started"})
|
|
21714
|
+
bb = self._ensure_blackboard()
|
|
21715
|
+
bb["status"] = "PLANNING"
|
|
21716
|
+
bb["plan"] = {"phase": "research", "findings": []}
|
|
21717
|
+
self.blackboard = bb
|
|
21718
|
+
|
|
21719
|
+
research_prompt = self._plan_mode_research_prompt()
|
|
21720
|
+
self._seed_plan_mode_explorer_context(research_prompt)
|
|
21721
|
+
|
|
21722
|
+
for r in range(PLAN_MODE_EXPLORER_MAX_ROUNDS):
|
|
21723
|
+
if self.cancel_requested:
|
|
21724
|
+
return
|
|
21725
|
+
step = self._plan_mode_explorer_turn(pinned_selection, round_idx=r)
|
|
21726
|
+
if step.get("status") in ("no-tools", "skip", "interrupted"):
|
|
21727
|
+
break
|
|
21728
|
+
self._plan_mode_update_findings(step)
|
|
21729
|
+
|
|
21730
|
+
# Phase 2: Manager 综合分析
|
|
21731
|
+
self._emit("status", {"summary": "plan-mode: synthesizing proposals"})
|
|
21732
|
+
bb = self._ensure_blackboard()
|
|
21733
|
+
if not isinstance(bb.get("plan"), dict):
|
|
21734
|
+
bb["plan"] = {"phase": "synthesis", "findings": []}
|
|
21735
|
+
bb["plan"]["phase"] = "synthesis"
|
|
21736
|
+
self.blackboard = bb
|
|
21737
|
+
|
|
21738
|
+
proposal = self._plan_mode_synthesize_proposal(pinned_selection)
|
|
21739
|
+
if not proposal or not proposal.get("options"):
|
|
21740
|
+
self._emit("status", {"summary": "plan-mode: synthesis failed, falling back to direct execution"})
|
|
21741
|
+
self.runtime_plan_mode_needed = False
|
|
21742
|
+
self.runtime_plan_approved = True
|
|
21743
|
+
return
|
|
21744
|
+
|
|
21745
|
+
# Phase 3: Emit 方案到前端
|
|
21746
|
+
self.runtime_plan_proposal = proposal
|
|
21747
|
+
bb = self._ensure_blackboard()
|
|
21748
|
+
if not isinstance(bb.get("plan"), dict):
|
|
21749
|
+
bb["plan"] = {"phase": "awaiting_choice", "findings": []}
|
|
21750
|
+
bb["plan"]["phase"] = "awaiting_choice"
|
|
21751
|
+
bb["plan"]["proposal"] = proposal
|
|
21752
|
+
self.blackboard = bb
|
|
21753
|
+
|
|
21754
|
+
plan_text = self._format_plan_proposal_markdown(proposal)
|
|
21755
|
+
self.messages.append({
|
|
21756
|
+
"role": "assistant",
|
|
21757
|
+
"content": plan_text,
|
|
21758
|
+
"ts": now_ts(),
|
|
21759
|
+
"agent_role": "planner",
|
|
21760
|
+
})
|
|
21761
|
+
self._emit("message", {
|
|
21762
|
+
"role": "assistant",
|
|
21763
|
+
"text": trim(plan_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
|
|
21764
|
+
"summary": "plan-mode proposal",
|
|
21765
|
+
"agent_role": "planner",
|
|
21766
|
+
})
|
|
21767
|
+
self._emit("status", {"summary": "plan-mode: awaiting user choice"})
|
|
21768
|
+
|
|
21769
|
+
def _plan_mode_research_prompt(self) -> str:
|
|
21770
|
+
goal = trim(str(self.runtime_reclassify_goal or self._latest_user_goal_text() or ""), 4000)
|
|
21771
|
+
lang_note = model_language_instruction(self.ui_language)
|
|
21772
|
+
os_note = _detect_os_shell_instruction()
|
|
21773
|
+
return (
|
|
21774
|
+
f"You are in plan-mode research phase. Your task is to analyze the following request "
|
|
21775
|
+
f"WITHOUT making any modifications.\n\n"
|
|
21776
|
+
f"## User Request\n{goal}\n\n"
|
|
21777
|
+
f"## Instructions\n"
|
|
21778
|
+
f"1. Analyze the impact scope — which files, modules, and systems are affected\n"
|
|
21779
|
+
f"2. Identify key files that need to be read and understood\n"
|
|
21780
|
+
f"3. Assess risks and potential side effects\n"
|
|
21781
|
+
f"4. Note any ambiguities or decisions that need user input\n"
|
|
21782
|
+
f"5. DO NOT write, edit, or create any files. Read-only analysis only.\n"
|
|
21783
|
+
f"6. Write your findings to the blackboard under 'plan.findings'.\n\n"
|
|
21784
|
+
f"Workspace: {self.files_root}\n"
|
|
21785
|
+
f"{os_note}\n"
|
|
21786
|
+
f"{lang_note}"
|
|
21787
|
+
)
|
|
21788
|
+
|
|
21789
|
+
def _seed_plan_mode_explorer_context(self, research_prompt: str):
|
|
21790
|
+
os_note = _detect_os_shell_instruction()
|
|
21791
|
+
self._append_agent_context_message("explorer", {
|
|
21792
|
+
"role": "system",
|
|
21793
|
+
"content": (
|
|
21794
|
+
"You are Explorer in plan-mode (read-only research). "
|
|
21795
|
+
"Analyze the codebase to understand the task scope. "
|
|
21796
|
+
"Do NOT modify any files. Use read_file, bash (read-only commands), "
|
|
21797
|
+
"and blackboard tools only. "
|
|
21798
|
+
f"{os_note} "
|
|
21799
|
+
f"{model_language_instruction(self.ui_language)}"
|
|
21800
|
+
),
|
|
21801
|
+
"ts": now_ts(),
|
|
21802
|
+
"agent_role": "explorer",
|
|
21803
|
+
}, mirror_to_global=False)
|
|
21804
|
+
self._append_agent_context_message("explorer", {
|
|
21805
|
+
"role": "user",
|
|
21806
|
+
"content": research_prompt,
|
|
21807
|
+
"ts": now_ts(),
|
|
21808
|
+
"agent_role": "explorer",
|
|
21809
|
+
}, mirror_to_global=False)
|
|
21810
|
+
|
|
21811
|
+
def _plan_mode_explorer_turn(self, pinned_selection: str, round_idx: int) -> dict:
|
|
21812
|
+
ctx = self._agent_context("explorer")
|
|
21813
|
+
if not ctx:
|
|
21814
|
+
return {"status": "skip", "reason": "empty-context"}
|
|
21815
|
+
# Build filtered tool list (read-only allowlist)
|
|
21816
|
+
filtered_tools = []
|
|
21817
|
+
for tool in TOOLS:
|
|
21818
|
+
fn = tool.get("function", {}) if isinstance(tool, dict) else {}
|
|
21819
|
+
name = str(fn.get("name", "")).strip()
|
|
21820
|
+
if name in PLAN_MODE_RESEARCH_TOOL_ALLOWLIST:
|
|
21821
|
+
filtered_tools.append(tool)
|
|
21822
|
+
if not filtered_tools:
|
|
21823
|
+
filtered_tools = self._tools_for_agent("explorer")
|
|
21824
|
+
with self.lock:
|
|
21825
|
+
self.current_phase = f"plan-mode:explorer:round-{round_idx}"
|
|
21826
|
+
self.current_tool_name = ""
|
|
21827
|
+
self.active_agent_role = "explorer"
|
|
21828
|
+
response = self._chat_with_same_model_retry(
|
|
21829
|
+
ctx,
|
|
21830
|
+
tools=filtered_tools,
|
|
21831
|
+
system=(
|
|
21832
|
+
"You are Explorer in plan-mode research. Read-only analysis. "
|
|
21833
|
+
"Do NOT create, write, or edit files. "
|
|
21834
|
+
f"Workspace: {self.files_root}. "
|
|
21835
|
+
f"{_detect_os_shell_instruction()} "
|
|
21836
|
+
f"{model_language_instruction(self.ui_language)}"
|
|
21837
|
+
),
|
|
21838
|
+
max_tokens=self.max_output_tokens,
|
|
21839
|
+
think=False,
|
|
21840
|
+
stream_thinking=False,
|
|
21841
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
21842
|
+
pinned_selection=pinned_selection,
|
|
21843
|
+
context_label=f"plan-mode explorer round {round_idx}",
|
|
21844
|
+
retries=MODEL_OUTPUT_RETRY_TIMES,
|
|
21845
|
+
)
|
|
21846
|
+
text = str(response.get("content") or "")
|
|
21847
|
+
tool_calls = response.get("tool_calls", [])
|
|
21848
|
+
text, _ = self._sanitize_assistant_text_for_runtime(text, tool_calls)
|
|
21849
|
+
assistant = {"role": "assistant", "content": text, "ts": now_ts(), "agent_role": "explorer"}
|
|
21850
|
+
if tool_calls:
|
|
21851
|
+
assistant["tool_calls"] = [
|
|
21852
|
+
{
|
|
21853
|
+
"id": tc["id"],
|
|
21854
|
+
"type": "function",
|
|
21855
|
+
"function": {
|
|
21856
|
+
"name": tc["function"]["name"],
|
|
21857
|
+
"arguments": json_dumps(tc["function"]["arguments"]),
|
|
21858
|
+
},
|
|
21859
|
+
}
|
|
21860
|
+
for tc in tool_calls
|
|
21861
|
+
]
|
|
21862
|
+
self._append_agent_context_message("explorer", assistant, mirror_to_global=False)
|
|
21863
|
+
# B4: Emit planner research bubble for user visibility
|
|
21864
|
+
if text.strip():
|
|
21865
|
+
self._emit("message", {
|
|
21866
|
+
"role": "assistant",
|
|
21867
|
+
"agent_role": "planner",
|
|
21868
|
+
"text": trim(text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
|
|
21869
|
+
"summary": f"plan-mode: research round {round_idx + 1}",
|
|
21870
|
+
})
|
|
21871
|
+
if not tool_calls:
|
|
21872
|
+
return {"status": "no-tools", "text": text}
|
|
21873
|
+
# Execute tool calls (read-only)
|
|
21874
|
+
for tc in tool_calls:
|
|
21875
|
+
if self.cancel_requested:
|
|
21876
|
+
return {"status": "interrupted"}
|
|
21877
|
+
fn_name = tc["function"]["name"]
|
|
21878
|
+
fn_args = tc["function"]["arguments"]
|
|
21879
|
+
# Block write operations in bash
|
|
21880
|
+
if fn_name == "bash":
|
|
21881
|
+
cmd = str(fn_args.get("command", "") if isinstance(fn_args, dict) else "")
|
|
21882
|
+
cmd_lower = cmd.lower().strip()
|
|
21883
|
+
write_indicators = ("rm ", "mv ", "cp ", ">", ">>", "tee ", "dd ", "write_file", "edit_file", "mkdir ", "touch ")
|
|
21884
|
+
if any(cmd_lower.startswith(w) or f" {w}" in cmd_lower for w in write_indicators):
|
|
21885
|
+
result_content = "[plan-mode] Write operations are blocked during research phase."
|
|
21886
|
+
self._append_agent_context_message("explorer", {
|
|
21887
|
+
"role": "tool",
|
|
21888
|
+
"tool_call_id": tc["id"],
|
|
21889
|
+
"content": result_content,
|
|
21890
|
+
"ts": now_ts(),
|
|
21891
|
+
"agent_role": "explorer",
|
|
21892
|
+
}, mirror_to_global=False)
|
|
21893
|
+
continue
|
|
21894
|
+
try:
|
|
21895
|
+
raw_output = self._dispatch_tool(fn_name, fn_args, agent_role="explorer")
|
|
21896
|
+
except Exception as exc:
|
|
21897
|
+
raw_output = f"Error: {exc}"
|
|
21898
|
+
result_content = str(raw_output or "")
|
|
21899
|
+
self._append_agent_context_message("explorer", {
|
|
21900
|
+
"role": "tool",
|
|
21901
|
+
"tool_call_id": tc["id"],
|
|
21902
|
+
"content": trim(result_content, 8000),
|
|
21903
|
+
"ts": now_ts(),
|
|
21904
|
+
"agent_role": "explorer",
|
|
21905
|
+
}, mirror_to_global=False)
|
|
21906
|
+
# B5: Emit tool result as planner bubble
|
|
21907
|
+
self._emit("tool_result", {
|
|
21908
|
+
"name": fn_name,
|
|
21909
|
+
"agent_role": "planner",
|
|
21910
|
+
"result": trim(result_content, 500),
|
|
21911
|
+
"summary": f"plan-mode research: {fn_name}",
|
|
21912
|
+
})
|
|
21913
|
+
return {"status": "ok", "tool_count": len(tool_calls), "text": text}
|
|
21914
|
+
|
|
21915
|
+
def _plan_mode_update_findings(self, step: dict):
|
|
21916
|
+
bb = self._ensure_blackboard()
|
|
21917
|
+
plan = bb.get("plan", {})
|
|
21918
|
+
if not isinstance(plan, dict):
|
|
21919
|
+
plan = {"phase": "research", "findings": []}
|
|
21920
|
+
findings = plan.get("findings", [])
|
|
21921
|
+
if not isinstance(findings, list):
|
|
21922
|
+
findings = []
|
|
21923
|
+
text = trim(str(step.get("text", "") or ""), 2000)
|
|
21924
|
+
if text:
|
|
21925
|
+
findings.append({"round": len(findings), "content": text, "ts": now_ts()})
|
|
21926
|
+
findings = findings[-20:]
|
|
21927
|
+
plan["findings"] = findings
|
|
21928
|
+
bb["plan"] = plan
|
|
21929
|
+
self.blackboard = bb
|
|
21930
|
+
# B6: Emit findings status as planner
|
|
21931
|
+
if text:
|
|
21932
|
+
self._emit("status", {
|
|
21933
|
+
"summary": f"plan-mode: finding #{len(findings)} collected",
|
|
21934
|
+
"agent_role": "planner",
|
|
21935
|
+
})
|
|
21936
|
+
|
|
21937
|
+
def _plan_mode_synthesize_proposal(self, pinned_selection: str) -> dict:
|
|
21938
|
+
bb = self._ensure_blackboard()
|
|
21939
|
+
plan_data = bb.get("plan", {})
|
|
21940
|
+
findings = plan_data.get("findings", []) if isinstance(plan_data, dict) else []
|
|
21941
|
+
findings_text = "\n\n".join(
|
|
21942
|
+
f"### Finding {i+1}\n{f.get('content', '')}"
|
|
21943
|
+
for i, f in enumerate(findings) if isinstance(f, dict)
|
|
21944
|
+
)
|
|
21945
|
+
goal = trim(str(self.runtime_reclassify_goal or self._latest_user_goal_text() or ""), 4000)
|
|
21946
|
+
synthesis_prompt = (
|
|
21947
|
+
f"Based on the research findings below, generate a structured plan proposal "
|
|
21948
|
+
f"with up to {PLAN_MODE_MAX_OPTIONS} options for the user to choose from.\n\n"
|
|
21949
|
+
f"## User Request\n{goal}\n\n"
|
|
21950
|
+
f"## Research Findings\n{trim(findings_text, 6000)}\n\n"
|
|
21951
|
+
f"## Instructions\n"
|
|
21952
|
+
f"Call the submit_plan_proposal tool with:\n"
|
|
21953
|
+
f"- context: brief background analysis\n"
|
|
21954
|
+
f"- options: array of 1-{PLAN_MODE_MAX_OPTIONS} options, each with id (A/B/C), title, summary, steps, pros, cons, risk\n"
|
|
21955
|
+
f"- recommended: id of the recommended option\n\n"
|
|
21956
|
+
f"Make options meaningfully different (e.g. different approaches, scope levels, or trade-offs).\n"
|
|
21957
|
+
f"{model_language_instruction(self.ui_language)}"
|
|
21958
|
+
)
|
|
21959
|
+
synthesis_ctx = [
|
|
21960
|
+
{"role": "system", "content": "You are a technical architect synthesizing research into actionable plans.", "ts": now_ts()},
|
|
21961
|
+
{"role": "user", "content": synthesis_prompt, "ts": now_ts()},
|
|
21962
|
+
]
|
|
21963
|
+
response = self._chat_with_same_model_retry(
|
|
21964
|
+
synthesis_ctx,
|
|
21965
|
+
tools=self._plan_mode_synthesis_tools(),
|
|
21966
|
+
system="Generate a structured plan proposal. Use the submit_plan_proposal tool.",
|
|
21967
|
+
max_tokens=PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS,
|
|
21968
|
+
think=False,
|
|
21969
|
+
stream_thinking=False,
|
|
21970
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
21971
|
+
pinned_selection=pinned_selection,
|
|
21972
|
+
context_label="plan-mode synthesis",
|
|
21973
|
+
retries=MODEL_OUTPUT_RETRY_TIMES,
|
|
21974
|
+
)
|
|
21975
|
+
tool_calls = response.get("tool_calls", [])
|
|
21976
|
+
for tc in tool_calls:
|
|
21977
|
+
if tc.get("function", {}).get("name") == "submit_plan_proposal":
|
|
21978
|
+
args = tc["function"].get("arguments", {})
|
|
21979
|
+
if isinstance(args, dict) and args.get("options"):
|
|
21980
|
+
return dict(args)
|
|
21981
|
+
return {}
|
|
21982
|
+
|
|
21983
|
+
def _plan_mode_synthesis_tools(self) -> list:
|
|
21984
|
+
return [tool_def(
|
|
21985
|
+
"submit_plan_proposal",
|
|
21986
|
+
"Submit a structured plan proposal with multiple options for user review.",
|
|
21987
|
+
{
|
|
21988
|
+
"context": {"type": "string", "description": "Background analysis of the task"},
|
|
21989
|
+
"options": {
|
|
21990
|
+
"type": "array",
|
|
21991
|
+
"items": {
|
|
21992
|
+
"type": "object",
|
|
21993
|
+
"properties": {
|
|
21994
|
+
"id": {"type": "string"},
|
|
21995
|
+
"title": {"type": "string"},
|
|
21996
|
+
"summary": {"type": "string"},
|
|
21997
|
+
"steps": {"type": "array", "items": {"type": "string"}},
|
|
21998
|
+
"pros": {"type": "string"},
|
|
21999
|
+
"cons": {"type": "string"},
|
|
22000
|
+
"risk": {"type": "string", "enum": ["low", "medium", "high"]},
|
|
22001
|
+
},
|
|
22002
|
+
"required": ["id", "title", "summary", "steps"],
|
|
22003
|
+
},
|
|
22004
|
+
},
|
|
22005
|
+
"recommended": {"type": "string", "description": "ID of recommended option"},
|
|
22006
|
+
},
|
|
22007
|
+
["context", "options", "recommended"],
|
|
22008
|
+
)]
|
|
22009
|
+
|
|
22010
|
+
def _format_plan_proposal_markdown(self, proposal: dict) -> str:
|
|
22011
|
+
lines = ["## 📋 执行方案\n"]
|
|
22012
|
+
context = str(proposal.get("context", "") or "").strip()
|
|
22013
|
+
if context:
|
|
22014
|
+
lines.append(f"### 背景分析\n{context}\n")
|
|
22015
|
+
recommended = str(proposal.get("recommended", "") or "").strip()
|
|
22016
|
+
options = proposal.get("options", [])
|
|
22017
|
+
if not isinstance(options, list):
|
|
22018
|
+
options = []
|
|
22019
|
+
for opt in options[:PLAN_MODE_MAX_OPTIONS]:
|
|
22020
|
+
if not isinstance(opt, dict):
|
|
22021
|
+
continue
|
|
22022
|
+
opt_id = str(opt.get("id", "") or "").strip()
|
|
22023
|
+
title = str(opt.get("title", "") or "").strip()
|
|
22024
|
+
is_recommended = opt_id == recommended
|
|
22025
|
+
header = f"### 方案 {opt_id}: {title}"
|
|
22026
|
+
if is_recommended:
|
|
22027
|
+
header += " ⭐推荐"
|
|
22028
|
+
lines.append(header)
|
|
22029
|
+
summary = str(opt.get("summary", "") or "").strip()
|
|
22030
|
+
if summary:
|
|
22031
|
+
lines.append(summary)
|
|
22032
|
+
steps = opt.get("steps", [])
|
|
22033
|
+
if isinstance(steps, list) and steps:
|
|
22034
|
+
lines.append("\n**步骤:**")
|
|
22035
|
+
for i, s in enumerate(steps):
|
|
22036
|
+
lines.append(f"{i+1}. {s}")
|
|
22037
|
+
pros = str(opt.get("pros", "") or "").strip()
|
|
22038
|
+
if pros:
|
|
22039
|
+
lines.append(f"\n**优势:** {pros}")
|
|
22040
|
+
cons = str(opt.get("cons", "") or "").strip()
|
|
22041
|
+
if cons:
|
|
22042
|
+
lines.append(f"**劣势:** {cons}")
|
|
22043
|
+
risk = str(opt.get("risk", "") or "").strip()
|
|
22044
|
+
if risk:
|
|
22045
|
+
lines.append(f"**风险:** {risk}")
|
|
22046
|
+
lines.append("")
|
|
22047
|
+
lines.append("---")
|
|
22048
|
+
lines.append('请回复选择(如"方案A"、"A"、"选1"),或输入修改意见。')
|
|
22049
|
+
return "\n".join(lines)
|
|
22050
|
+
|
|
22051
|
+
def _parse_plan_choice(self, text: str, proposal: dict) -> str:
|
|
22052
|
+
if not text or not proposal:
|
|
22053
|
+
return ""
|
|
22054
|
+
low = text.strip().lower()
|
|
22055
|
+
options = proposal.get("options", [])
|
|
22056
|
+
if not isinstance(options, list) or not options:
|
|
22057
|
+
return ""
|
|
22058
|
+
option_ids = [str(o.get("id", "")).strip() for o in options if isinstance(o, dict)]
|
|
22059
|
+
# Direct ID match: "A", "B", "C"
|
|
22060
|
+
if low.upper() in option_ids:
|
|
22061
|
+
return low.upper()
|
|
22062
|
+
# "方案A", "方案 A", "option A"
|
|
22063
|
+
import re
|
|
22064
|
+
m = re.search(r'(?:方案|option|选项)\s*([a-zA-Z0-9])', low, re.IGNORECASE)
|
|
22065
|
+
if m:
|
|
22066
|
+
candidate = m.group(1).upper()
|
|
22067
|
+
if candidate in option_ids:
|
|
22068
|
+
return candidate
|
|
22069
|
+
# "选1", "第1个", "第一个"
|
|
22070
|
+
num_map = {"一": "1", "二": "2", "三": "3", "1": "1", "2": "2", "3": "3"}
|
|
22071
|
+
m2 = re.search(r'(?:选|第)\s*([一二三1-3])', low)
|
|
22072
|
+
if m2:
|
|
22073
|
+
idx_str = num_map.get(m2.group(1), "")
|
|
22074
|
+
if idx_str:
|
|
22075
|
+
idx = int(idx_str) - 1
|
|
22076
|
+
if 0 <= idx < len(option_ids):
|
|
22077
|
+
return option_ids[idx]
|
|
22078
|
+
# "继续"/"确认"/"推荐" → pick recommended
|
|
22079
|
+
recommended = str(proposal.get("recommended", "") or "").strip()
|
|
22080
|
+
confirm_tokens = ("继续", "确认", "推荐", "推荐方案", "go", "proceed", "continue", "yes", "ok")
|
|
22081
|
+
if any(tok in low for tok in confirm_tokens) and recommended:
|
|
22082
|
+
return recommended
|
|
22083
|
+
return ""
|
|
22084
|
+
|
|
22085
|
+
def _inject_plan_into_context(self, choice_id: str):
|
|
22086
|
+
chosen = next(
|
|
22087
|
+
(o for o in self.runtime_plan_proposal.get("options", [])
|
|
22088
|
+
if isinstance(o, dict) and o.get("id") == choice_id),
|
|
22089
|
+
None,
|
|
22090
|
+
)
|
|
22091
|
+
if not chosen:
|
|
22092
|
+
return
|
|
22093
|
+
steps_text = "\n".join(f"{i+1}. {s}" for i, s in enumerate(chosen.get("steps", [])))
|
|
22094
|
+
plan_msg = (
|
|
22095
|
+
f"[approved-plan] Execute the following plan:\n"
|
|
22096
|
+
f"## {chosen.get('title', '')}\n"
|
|
22097
|
+
f"{chosen.get('summary', '')}\n\n"
|
|
22098
|
+
f"### Steps:\n{steps_text}\n\n"
|
|
22099
|
+
f"Follow these steps. Use tools to implement each step concretely."
|
|
22100
|
+
)
|
|
22101
|
+
self.messages.append({
|
|
22102
|
+
"role": "system",
|
|
22103
|
+
"content": plan_msg,
|
|
22104
|
+
"ts": now_ts(),
|
|
22105
|
+
})
|
|
22106
|
+
if self._is_multi_agent_mode():
|
|
22107
|
+
for role in ("explorer", "developer"):
|
|
22108
|
+
self._append_agent_context_message(role, {
|
|
22109
|
+
"role": "system",
|
|
22110
|
+
"content": plan_msg,
|
|
22111
|
+
"ts": now_ts(),
|
|
22112
|
+
"agent_role": role,
|
|
22113
|
+
}, mirror_to_global=False)
|
|
22114
|
+
bb = self._ensure_blackboard()
|
|
22115
|
+
bb["plan"] = {"phase": "executing", "chosen": choice_id, "steps": chosen.get("steps", [])}
|
|
22116
|
+
self.blackboard = bb
|
|
22117
|
+
self._blackboard_history("manager", f"plan approved: option {choice_id} — {chosen.get('title', '')}")
|
|
22118
|
+
# Auto-create todos from plan steps → write into bb["project_todos"]
|
|
22119
|
+
steps = chosen.get("steps", [])
|
|
22120
|
+
if steps and isinstance(steps, list):
|
|
22121
|
+
plan_todos = []
|
|
22122
|
+
for i, step in enumerate(steps):
|
|
22123
|
+
step_text = trim(str(step or "").strip(), 400)
|
|
22124
|
+
if not step_text:
|
|
22125
|
+
continue
|
|
22126
|
+
plan_todos.append({
|
|
22127
|
+
"id": f"pt:{i:03d}",
|
|
22128
|
+
"content": step_text,
|
|
22129
|
+
"status": "in_progress" if i == 0 else "pending",
|
|
22130
|
+
"category": "plan_step",
|
|
22131
|
+
"plan_step_index": i,
|
|
22132
|
+
"created_at": float(now_ts()),
|
|
22133
|
+
"completed_at": None,
|
|
22134
|
+
"completed_by": "",
|
|
22135
|
+
"evidence": "",
|
|
22136
|
+
})
|
|
22137
|
+
if plan_todos:
|
|
22138
|
+
bb["project_todos"] = plan_todos[:10]
|
|
22139
|
+
bb["plan_step_cursor"] = 0
|
|
22140
|
+
bb["plan_step_total"] = len(plan_todos)
|
|
22141
|
+
self.blackboard = bb
|
|
22142
|
+
self._blackboard_touch()
|
|
22143
|
+
# 同步到 UI todo
|
|
22144
|
+
try:
|
|
22145
|
+
self.todo.update([
|
|
22146
|
+
{"content": t["content"], "status": t["status"], "owner": "developer"}
|
|
22147
|
+
for t in plan_todos[:10]
|
|
22148
|
+
])
|
|
22149
|
+
except Exception:
|
|
22150
|
+
pass
|
|
22151
|
+
|
|
19897
22152
|
def _agent_worker(self):
|
|
19898
22153
|
single_role = "developer"
|
|
19899
22154
|
try:
|
|
@@ -19944,6 +22199,10 @@ class SessionState:
|
|
|
19944
22199
|
{"summary": "level-5 requires user confirmation before next actions"},
|
|
19945
22200
|
)
|
|
19946
22201
|
return
|
|
22202
|
+
# ── Plan Mode 检查 ──
|
|
22203
|
+
if bool(self.runtime_plan_mode_needed) and not bool(self.runtime_plan_approved):
|
|
22204
|
+
self._plan_mode_worker(pinned_selection=pinned_selection)
|
|
22205
|
+
return
|
|
19947
22206
|
if self._is_multi_agent_mode():
|
|
19948
22207
|
self._seed_multi_agent_contexts_if_needed(self.runtime_reclassify_goal or "")
|
|
19949
22208
|
self._emit(
|
|
@@ -20028,6 +22287,10 @@ class SessionState:
|
|
|
20028
22287
|
)
|
|
20029
22288
|
break
|
|
20030
22289
|
self._apply_auto_compact_if_needed("auto")
|
|
22290
|
+
# Periodic checkpoint in single-agent loop
|
|
22291
|
+
_sa_round = int(getattr(self, "agent_round_index", 0) or 0)
|
|
22292
|
+
if _sa_round > 0 and _sa_round % CHECKPOINT_INTERVAL_ROUNDS == 0:
|
|
22293
|
+
self._maybe_create_checkpoint()
|
|
20031
22294
|
notifs = self.bg.drain()
|
|
20032
22295
|
if notifs:
|
|
20033
22296
|
text = "\n".join(f"[bg:{n['task_id']}] {n['status']}: {n['result']}" for n in notifs)
|
|
@@ -20251,7 +22514,7 @@ class SessionState:
|
|
|
20251
22514
|
for tc in tool_calls
|
|
20252
22515
|
]
|
|
20253
22516
|
self.messages.append(assistant)
|
|
20254
|
-
if text.strip() or thinking_text:
|
|
22517
|
+
if (text.strip() or thinking_text) and not tool_calls:
|
|
20255
22518
|
emit_text = text if text.strip() else "[thinking-only output]"
|
|
20256
22519
|
emit_summary = "assistant message" if text.strip() else "assistant thinking-only message"
|
|
20257
22520
|
self._emit(
|
|
@@ -20409,6 +22672,8 @@ class SessionState:
|
|
|
20409
22672
|
fault_counter = 0
|
|
20410
22673
|
last_fault_reason = ""
|
|
20411
22674
|
self._prune_runtime_retry_hints()
|
|
22675
|
+
if self.todo.has_open_items():
|
|
22676
|
+
self._mark_all_done_silently("single-mode endpoint exit")
|
|
20412
22677
|
self._emit(
|
|
20413
22678
|
"status",
|
|
20414
22679
|
{
|
|
@@ -20426,6 +22691,8 @@ class SessionState:
|
|
|
20426
22691
|
fault_counter = 0
|
|
20427
22692
|
last_fault_reason = ""
|
|
20428
22693
|
self._prune_runtime_retry_hints()
|
|
22694
|
+
if self.todo.has_open_items():
|
|
22695
|
+
self._mark_all_done_silently("single-mode conclusive/substantial exit")
|
|
20429
22696
|
self._emit(
|
|
20430
22697
|
"status",
|
|
20431
22698
|
{
|
|
@@ -20763,6 +23030,25 @@ class SessionState:
|
|
|
20763
23030
|
"ok": not str(output).startswith("Error:"),
|
|
20764
23031
|
}
|
|
20765
23032
|
)
|
|
23033
|
+
# Failure ledger: record tool call and detect errors (single-agent, unified)
|
|
23034
|
+
self._ledger_record_tool_call(name, args if isinstance(args, dict) else {})
|
|
23035
|
+
_sa_ok = not str(output or "").startswith("Error")
|
|
23036
|
+
self._process_tool_result_errors(name, args if isinstance(args, dict) else {}, output, _sa_ok, "single")
|
|
23037
|
+
if name in ("edit_file", "write_file") and isinstance(args, dict):
|
|
23038
|
+
_fl = self._ensure_blackboard().get("failure_ledger", {})
|
|
23039
|
+
_all_errors = _fl.get("errors", []) if isinstance(_fl, dict) else []
|
|
23040
|
+
_edit_path = str(args.get("path", "") or "").strip()
|
|
23041
|
+
if _all_errors and _edit_path:
|
|
23042
|
+
_matching = [e for e in _all_errors if _edit_path.endswith(str(e.get("file", "") or "")) or str(e.get("file", "") or "").endswith(_edit_path)]
|
|
23043
|
+
if _matching:
|
|
23044
|
+
_fix_desc = trim(str(args.get("new_text", "") or args.get("content", "") or ""), 300)
|
|
23045
|
+
self._ledger_record_fix_attempt(
|
|
23046
|
+
_edit_path,
|
|
23047
|
+
trim(str(_matching[-1].get("error_msg", "") or ""), 300),
|
|
23048
|
+
_fix_desc,
|
|
23049
|
+
"pending",
|
|
23050
|
+
"single",
|
|
23051
|
+
)
|
|
20766
23052
|
self._emit("tool_result", {"name": name, "result": trim(output, 500), "summary": f"tool done: {name}"})
|
|
20767
23053
|
if int(tool_error_streaks.get(tool_key, 0) or 0) >= HARD_BREAK_TOOL_ERROR_THRESHOLD:
|
|
20768
23054
|
stop_due_to_hard_break = True
|
|
@@ -20803,6 +23089,8 @@ class SessionState:
|
|
|
20803
23089
|
state_changed=bool(single_watchdog_after_fp != single_watchdog_before_fp),
|
|
20804
23090
|
pinned_selection=pinned_selection,
|
|
20805
23091
|
)
|
|
23092
|
+
# Single-agent plan step tracking: sync todos and auto-advance
|
|
23093
|
+
self._single_agent_plan_step_check(single_round_tool_results)
|
|
20806
23094
|
if stop_due_to_hard_break:
|
|
20807
23095
|
note = (
|
|
20808
23096
|
"Execution paused after repeated tool/recovery failures. "
|
|
@@ -21014,6 +23302,10 @@ class SessionState:
|
|
|
21014
23302
|
except Exception as exc:
|
|
21015
23303
|
self._emit("error", {"summary": f"agent error: {exc}", "trace": traceback.format_exc()})
|
|
21016
23304
|
finally:
|
|
23305
|
+
if self.todo.has_open_items() and not self.cancel_requested:
|
|
23306
|
+
_last = self._latest_agent_assistant_text(single_role) or ""
|
|
23307
|
+
if self._looks_like_conclusive_reply(_last):
|
|
23308
|
+
self._mark_all_done_silently(f"single-mode conclusive exit by {single_role}")
|
|
21017
23309
|
dropped_pending_inputs = 0
|
|
21018
23310
|
removed_runtime_hints = 0
|
|
21019
23311
|
with self.lock:
|
|
@@ -21321,6 +23613,7 @@ class SessionState:
|
|
|
21321
23613
|
"context_token_upper_bound": int(self.context_token_upper_bound),
|
|
21322
23614
|
"context_token_limit_config": int(self.max_context_token_limit),
|
|
21323
23615
|
"context_token_limit_locked": bool(self.context_limit_locked),
|
|
23616
|
+
"plan_mode_preference": str(self.plan_mode_user_preference or "auto"),
|
|
21324
23617
|
"context_tokens_estimate": int(ctx.get("used", 0)),
|
|
21325
23618
|
"context_left_tokens": int(ctx.get("left", 0)),
|
|
21326
23619
|
"context_left_percent": round(float(ctx.get("left_percent", 0.0)), 2),
|
|
@@ -21526,6 +23819,7 @@ class SessionManager:
|
|
|
21526
23819
|
self.arbiter_max_tokens = max(24, min(256, int(arbiter_max_tokens or ARBITER_DEFAULT_MAX_TOKENS)))
|
|
21527
23820
|
self.arbiter_temperature = max(0.0, min(1.0, float(arbiter_temperature if arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)))
|
|
21528
23821
|
self.execution_mode = normalize_execution_mode(execution_mode, default=EXECUTION_MODE_SYNC)
|
|
23822
|
+
self.single_advance_prompt_enhance = False
|
|
21529
23823
|
env_ok, env_tags, _ = probe_ollama_environment(ollama_base)
|
|
21530
23824
|
self.ollama_env_available = bool(env_ok)
|
|
21531
23825
|
self.ollama_env_tags: list[str] = list(env_tags)
|
|
@@ -21792,6 +24086,7 @@ class SessionManager:
|
|
|
21792
24086
|
min(1.0, float(self.arbiter_temperature if self.arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)),
|
|
21793
24087
|
)
|
|
21794
24088
|
sess.execution_mode = normalize_execution_mode(self.execution_mode, default=EXECUTION_MODE_SYNC)
|
|
24089
|
+
sess.single_advance_prompt_enhance = bool(self.single_advance_prompt_enhance)
|
|
21795
24090
|
sess._apply_active_profile()
|
|
21796
24091
|
sess.updated_at = now_ts()
|
|
21797
24092
|
sess._persist()
|
|
@@ -22341,6 +24636,7 @@ window.MathJax={
|
|
|
22341
24636
|
<button id="sendBtn">Send</button>
|
|
22342
24637
|
<button id="interruptBtn" class="subtle">Interrupt</button>
|
|
22343
24638
|
<button id="compactBtn" class="subtle">Compact</button>
|
|
24639
|
+
<button id="planModeBtn" class="subtle" title="Toggle Plan Mode">Plan: Auto</button>
|
|
22344
24640
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
22345
24641
|
<div class="export-dropdown" style="position:relative;display:inline-block">
|
|
22346
24642
|
<button id="exportMenuBtn" class="subtle">Export ▾</button>
|
|
@@ -22498,11 +24794,13 @@ main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px)
|
|
|
22498
24794
|
.msg.assistant.agent-developer,.msg.system.agent-developer{background:#e9fbea;border:1px solid #bde6c4}
|
|
22499
24795
|
.msg.assistant.agent-reviewer,.msg.system.agent-reviewer{background:#fff6de;border:1px solid #f1d89a}
|
|
22500
24796
|
.msg.assistant.agent-manager,.msg.system.agent-manager{background:#f3e9ff;border:1px solid #d7bbff}
|
|
24797
|
+
.msg.assistant.agent-planner,.msg.system.agent-planner{background:#fde8e4;border:1px solid #f5b8ad}
|
|
22501
24798
|
.msg-agent-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:.68rem;font-weight:700;letter-spacing:.02em;margin:0 0 6px 0;border:1px solid #d6deea;background:#f5f8fd;color:#31465f}
|
|
22502
24799
|
.msg-agent-badge.explorer{background:#ffe6f0;border-color:#ffbdd5;color:#9b275e}
|
|
22503
24800
|
.msg-agent-badge.developer{background:#e6f8e8;border-color:#b8e2c0;color:#1c6a33}
|
|
22504
24801
|
.msg-agent-badge.reviewer{background:#fff3d4;border-color:#efd692;color:#8a6213}
|
|
22505
24802
|
.msg-agent-badge.manager{background:#efe4ff;border-color:#d2b6ff;color:#6e36b8}
|
|
24803
|
+
.msg-agent-badge.planner{background:#fde8e4;border-color:#f5b8ad;color:#c0392b}
|
|
22506
24804
|
.manager-delegate-card{border:1px solid #d7bbff;background:#f8f3ff;border-radius:10px;padding:10px}
|
|
22507
24805
|
.manager-delegate-head{font-weight:800;font-size:.86rem;color:#5a2fa6}
|
|
22508
24806
|
.manager-delegate-route{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0 8px}
|
|
@@ -22889,6 +25187,7 @@ function _deltaScheduleRender(flags={}){
|
|
|
22889
25187
|
if(S.deltaRenderRaf)return;
|
|
22890
25188
|
S.deltaRenderRaf=requestAnimationFrame(()=>{
|
|
22891
25189
|
S.deltaRenderRaf=0;
|
|
25190
|
+
if(S.refreshInFlight)return;
|
|
22892
25191
|
const needChat=!!S.deltaRenderChat;
|
|
22893
25192
|
const needBoards=!!S.deltaRenderBoards;
|
|
22894
25193
|
const needSessions=!!S.deltaRenderSessions;
|
|
@@ -23048,7 +25347,7 @@ function onRuntimeEvent(evt){
|
|
|
23048
25347
|
const typ=String(evt.type||'');
|
|
23049
25348
|
if(typ==='render_frame'){_renderBridgeEnqueue(evt.data||{});return{handled:true,needsSnapshot:false}}
|
|
23050
25349
|
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}}
|
|
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));
|
|
25350
|
+
if(typ==='compact'){scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);const reason=parseCompactReason(evt.data||{});if(reason==='auto'||reason.startsWith('truncation-rescue')){const pct=Number(evt.data?.context_left_percent_before);const left=Number(evt.data?.context_left_before);const limit=Number(evt.data?.context_limit_before);const pctTxt=Number.isFinite(pct)?pct.toFixed(1):'-';const leftTxt=Number.isFinite(left)&&Number.isFinite(limit)?`${left}/${limit}`:'-';showCompactToast(`${t('compact_auto')}:${pctTxt}% left (${leftTxt}) · delta-sync`)}_deltaAppendActivity(typ,evt.data||{},Number(evt?.ts||Date.now()/1000));return{handled:true,needsSnapshot:false}}
|
|
23052
25351
|
return _deltaApplyRuntimeEvent(evt);
|
|
23053
25352
|
}
|
|
23054
25353
|
function _deltaStartWatchdog(){
|
|
@@ -23463,6 +25762,8 @@ self.onmessage=(ev)=>{
|
|
|
23463
25762
|
slot.classList.remove('md-async-slot');
|
|
23464
25763
|
const msg=slot.closest('.msg');
|
|
23465
25764
|
if(msg){
|
|
25765
|
+
const vk=String(msg.getAttribute('data-vk')||'');
|
|
25766
|
+
if(vk){const newH=Math.max(48,Math.ceil(msg.getBoundingClientRect().height||msg.offsetHeight||0));if(newH>0){const oldH=Number(CHAT_VIRT.heights[vk]||0);if(!oldH||Math.abs(oldH-newH)>=3){CHAT_VIRT.heights[vk]=newH;CHAT_VIRT.heightVersion=Number(CHAT_VIRT.heightVersion||0)+1}}}
|
|
23466
25767
|
const req=String(msg.getAttribute('data-math-request')||'').trim();
|
|
23467
25768
|
if(req)_mathTypeset(msg,`chat:${req}`);
|
|
23468
25769
|
}else{
|
|
@@ -24142,7 +26443,7 @@ function _chatVirtCollectRows(){
|
|
|
24142
26443
|
const label=String(S.snap?.live_run_notice_label||'model call').trim()||'model call';
|
|
24143
26444
|
const startedAt=Number(S.snap?.live_run_notice_started_at||0);
|
|
24144
26445
|
const snapElapsed=Number(S.snap?.live_run_notice_elapsed||0);
|
|
24145
|
-
const elapsed=startedAt>0?
|
|
26446
|
+
const elapsed=startedAt>0?(Date.now()/1000)-startedAt:snapElapsed;
|
|
24146
26447
|
rows.push({
|
|
24147
26448
|
role:'assistant',
|
|
24148
26449
|
type:'live_run_notice',
|
|
@@ -24151,12 +26452,12 @@ function _chatVirtCollectRows(){
|
|
|
24151
26452
|
elapsed:elapsed,
|
|
24152
26453
|
startedAt:startedAt,
|
|
24153
26454
|
agent_role:activeAgentRole||undefined,
|
|
24154
|
-
_vk:`run:${activeAgentRole}:${label}
|
|
26455
|
+
_vk:`run:${activeAgentRole}:${label}`
|
|
24155
26456
|
});
|
|
24156
26457
|
}
|
|
24157
26458
|
return rows;
|
|
24158
26459
|
}
|
|
24159
|
-
function _chatVirtEstimatedHeight(row){const key=String(row?._vk||'');const known=Number(CHAT_VIRT.heights[key]||0);if(known>0)return known;const txtLen=String(row?.text||'').length;const thinkLen=String(row?.thinking||'').length;const base=Math.max(72,Math.min(560,80+Math.ceil(
|
|
26460
|
+
function _chatVirtEstimatedHeight(row){const key=String(row?._vk||'');const known=Number(CHAT_VIRT.heights[key]||0);if(known>0)return known;const txtLen=String(row?.text||'').length;const thinkLen=String(row?.thinking||'').length;const totalLen=txtLen+thinkLen;const base=totalLen>1200?Math.max(400,Math.min(3200,120+Math.ceil(totalLen/60)*18)):Math.max(72,Math.min(560,80+Math.ceil(totalLen/90)*20));return Math.max(base,Number(CHAT_VIRT.avgHeight||140))}
|
|
24160
26461
|
function _chatVirtBindPreviewButtons(root){if(!root)return;for(const btn of root.querySelectorAll('.msg-preview-btn')){btn.onclick=(ev)=>{ev.preventDefault();ev.stopPropagation();openPreviewTab(btn.getAttribute('data-preview-path')||'')}}}
|
|
24161
26462
|
function _chatVirtPruneHeightCache(rows){const keys=Object.keys(CHAT_VIRT.heights||{});if(keys.length<=CHAT_VIRT.maxCacheKeys)return;const keep=new Set(rows.map(x=>String(x?._vk||'')));for(const k of keys){if(!keep.has(k)){delete CHAT_VIRT.heights[k]}}}
|
|
24162
26463
|
function _chatVirtPoolForKind(kind){const key=String(kind||'text');let pool=CHAT_VIRT.poolByKind[key];if(!Array.isArray(pool)){pool=[];CHAT_VIRT.poolByKind[key]=pool}return pool}
|
|
@@ -24182,8 +26483,8 @@ function _chatVirtReleaseNode(node){
|
|
|
24182
26483
|
CHAT_VIRT.poolSize=Number(CHAT_VIRT.poolSize||0)+1;
|
|
24183
26484
|
}
|
|
24184
26485
|
function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
|
|
24185
|
-
function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager')?role:''}
|
|
24186
|
-
function _chatVirtAgentRoleLabel(role){if(role==='explorer')return'Explorer';if(role==='developer')return'Developer';if(role==='reviewer')return'Reviewer';if(role==='manager')return'Manager';return''}
|
|
26486
|
+
function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager'||role==='planner')?role:''}
|
|
26487
|
+
function _chatVirtAgentRoleLabel(role){if(role==='explorer')return'Explorer';if(role==='developer')return'Developer';if(role==='reviewer')return'Reviewer';if(role==='manager')return'Manager';if(role==='planner')return'Planner';return''}
|
|
24187
26488
|
function _stripLeadingAgentTitle(raw,agentRole){
|
|
24188
26489
|
let txt=String(raw||'').replace(/^\\uFEFF/,'').trimStart();
|
|
24189
26490
|
const role=_chatVirtAgentRoleKey(agentRole);
|
|
@@ -24737,6 +27038,7 @@ function renderFileExplorer(){const host=E('fileExplorer');if(!host)return;const
|
|
|
24737
27038
|
async function refreshFileExplorer(force=false){const sid=String(S.activeId||'').trim();if(!sid)return;const st=ensureFileExplorerState(sid);if(!st)return;const now=Date.now();if(st.inflight)return;if(!force&&st.tree&&(now-Number(st.fetchedAt||0)<1400))return;st.inflight=true;const btn=E('refreshFilesBtn');if(btn)btn.disabled=true;renderFileExplorer();try{const payload=await api(_fePath(sid));if(String(S.activeId||'')!==sid)return;st.tree=(payload&&typeof payload==='object'&&payload.tree&&typeof payload.tree==='object')?payload.tree:null;st.root=String(payload?.root||S.snap?.session_files_root||'');st.nodeCount=Number(payload?.node_count||0);st.truncated=!!payload?.truncated;st.maxNodes=Number(payload?.max_nodes||0);st.fetchedAt=Date.now();renderFileExplorer()}catch(err){if(String(S.activeId||'')===sid){const host=E('fileExplorer');if(host)host.innerHTML=`<div class=\"fe-empty mono\">${esc(err?.message||String(err))}</div>`}}finally{st.inflight=false;if(btn)btn.disabled=false}}
|
|
24738
27039
|
function renderBoards(){const uiState=S.staticMode?(S.frozen?'static':'live'):'live';E('status').textContent=`session=${S.snap?.id||'-'} | model=${S.snap?.model||'-'} | thinking=${S.snap?.thinking?'on':'off'} | thinking_stream=${S.snap?.thinking_stream?'on':'off'} | mode=${S.snap?.execution_mode||S.config?.execution_mode||'sync'} | active_agent=${S.snap?.agent_active_role||'-'} | bb=${S.snap?.blackboard?.status||'-'} | task=${S.snap?.blackboard?.task_profile?.task_type||'-'} | complexity=${S.snap?.blackboard?.task_profile?.complexity||'-'} | judgement=${S.snap?.blackboard?.manager_judgement?.progress||'-'} | budget=${S.snap?.blackboard?.task_profile?.round_budget??'-'} | remaining=${S.snap?.blackboard?.manager_judgement?.remaining_rounds??'-'} | bb_cycles=${S.snap?.blackboard?.manager_cycles??'-'} | round_limit=${S.snap?.max_agent_rounds||'-'} | round=${S.snap?.agent_round_index??'-'} | phase=${S.snap?.agent_phase||'idle'} | queued_inputs=${S.snap?.queued_user_inputs_count??0} | run_timeout=${S.snap?.max_run_seconds??'-'}s | ctx_used=${S.snap?.context_tokens_estimate??'-'} | ctx_limit=${S.snap?.context_token_upper_bound||'-'} | ctx_mode=${S.snap?.context_token_limit_locked?'manual-lock':'adaptive'} | ctx_left=${formatContextLeft(S.snap)} | truncation=${S.snap?.truncation_count||0} | trunc_retry=${S.snap?.live_truncation_attempts||0} | trunc_tokens~=${S.snap?.live_truncation_tokens||0} | archive=${S.snap?.compact_segments_count||0} | last_compact=${S.snap?.last_compact_reason||'-'} | ollama=${S.snap?.ollama_base_url||'-'} | files=${S.snap?.session_files_root||'-'} | ui_mode=${uiState} | ${S.snap?.running?'running':'idle'}`;
|
|
24739
27040
|
renderCtxLive(S.snap);
|
|
27041
|
+
const _pmBtn=E('planModeBtn');if(_pmBtn){const _pm=S.snap?.plan_mode_preference||'auto';_pmBtn.textContent='Plan: '+_pm.charAt(0).toUpperCase()+_pm.slice(1)}
|
|
24740
27042
|
_renderBridgeSyncFromSnapshot(S.snap||{});
|
|
24741
27043
|
setPanelHtml('todos',renderTodoBoard(S.snap?.todos||[]));
|
|
24742
27044
|
setPanelHtml('tasks',renderTaskBoard(S.snap?.tasks||[]));
|
|
@@ -24909,23 +27211,21 @@ async function refreshSnapshot(opt={}){
|
|
|
24909
27211
|
const chatEl=E('chat');
|
|
24910
27212
|
const scrolling=_chatVirtIsUserScrolling(chatEl);
|
|
24911
27213
|
const feedSig=feedSignature(S.snap);
|
|
24912
|
-
if(forceFull||feedSig!==S.lastFeedSig){
|
|
24913
|
-
S.lastFeedSig=feedSig;
|
|
24914
|
-
if(scrolling&&chatEl){
|
|
24915
|
-
_chatVirtDebounceWhileScrolling(chatEl,'_virtScrollSyncTimer',()=>renderChat('snapshot'));
|
|
24916
|
-
}else{
|
|
24917
|
-
if(chatEl)_chatVirtCancelDebounce(chatEl,'_virtScrollSyncTimer');
|
|
24918
|
-
renderChat();
|
|
24919
|
-
}
|
|
24920
|
-
}
|
|
24921
27214
|
const boardSig=boardsSignature(S.snap);
|
|
24922
|
-
|
|
24923
|
-
|
|
27215
|
+
const needChat=forceFull||feedSig!==S.lastFeedSig;
|
|
27216
|
+
const needBoards=forceFull||boardSig!==S.lastBoardsSig;
|
|
27217
|
+
if(needChat)S.lastFeedSig=feedSig;
|
|
27218
|
+
if(needBoards)S.lastBoardsSig=boardSig;
|
|
27219
|
+
if(needChat||needBoards){
|
|
27220
|
+
const doRender=()=>{
|
|
27221
|
+
if(needChat)renderChat('snapshot');
|
|
27222
|
+
if(needBoards)renderBoards();
|
|
27223
|
+
};
|
|
24924
27224
|
if(scrolling&&chatEl){
|
|
24925
|
-
_chatVirtDebounceWhileScrolling(chatEl,'
|
|
27225
|
+
_chatVirtDebounceWhileScrolling(chatEl,'_virtScrollSyncTimer',doRender);
|
|
24926
27226
|
}else{
|
|
24927
|
-
if(chatEl)_chatVirtCancelDebounce(chatEl,'_virtBoardsSyncTimer');
|
|
24928
|
-
|
|
27227
|
+
if(chatEl){_chatVirtCancelDebounce(chatEl,'_virtScrollSyncTimer');_chatVirtCancelDebounce(chatEl,'_virtBoardsSyncTimer');}
|
|
27228
|
+
doRender();
|
|
24929
27229
|
}
|
|
24930
27230
|
}
|
|
24931
27231
|
renderActivePreview(false);
|
|
@@ -25009,9 +27309,10 @@ async function sendMessage(){showError('');const t=E('prompt').value.trim();if(!
|
|
|
25009
27309
|
async function interruptRun(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/interrupt',{method:'POST'});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:140,allowWhenFrozen:true})}}
|
|
25010
27310
|
async function compactNow(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/compact',{method:'POST'});S.lastDeltaTs=Date.now();scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:180,allowWhenFrozen:true})}}
|
|
25011
27311
|
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})}}
|
|
27312
|
+
async function togglePlanMode(){if(!S.activeId)return;const states=['auto','on','off'];const current=S.snap?.plan_mode_preference||'auto';const next=states[(states.indexOf(current)+1)%states.length];try{await api('/api/sessions/'+S.activeId+'/config/plan-mode',{method:'POST',body:JSON.stringify({preference:next})});if(S.snap)S.snap.plan_mode_preference=next;const btn=E('planModeBtn');if(btn)btn.textContent='Plan: '+next.charAt(0).toUpperCase()+next.slice(1)}catch(err){showError(err.message||String(err))}}
|
|
25012
27313
|
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})}
|
|
25013
27314
|
function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
|
|
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})})})
|
|
27315
|
+
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('planModeBtn',togglePlanMode);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})})})
|
|
25015
27316
|
"""
|
|
25016
27317
|
|
|
25017
27318
|
APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
|
|
@@ -26027,6 +28328,8 @@ class AppContext:
|
|
|
26027
28328
|
cfg_max_output_tokens = cfg.get("max_output_tokens")
|
|
26028
28329
|
if cfg_max_output_tokens is not None:
|
|
26029
28330
|
self.max_output_tokens = max(256, int(cfg_max_output_tokens))
|
|
28331
|
+
if "single_advance_prompt_enhance" in cfg:
|
|
28332
|
+
self.single_advance_prompt_enhance = bool(cfg["single_advance_prompt_enhance"])
|
|
26030
28333
|
|
|
26031
28334
|
def normalized_profiles() -> tuple[dict[str, dict], str]:
|
|
26032
28335
|
rows: dict[str, dict] = {}
|
|
@@ -27330,6 +29633,20 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
27330
29633
|
return self._send_json({"error": "session not found"}, status=404)
|
|
27331
29634
|
sess.interrupt()
|
|
27332
29635
|
return self._send_json({"ok": True})
|
|
29636
|
+
m = re.match(r"^/api/sessions/([^/]+)/config/plan-mode$", path)
|
|
29637
|
+
if m:
|
|
29638
|
+
sess = mgr.get(m.group(1))
|
|
29639
|
+
if not sess:
|
|
29640
|
+
return self._send_json({"error": "session not found"}, status=404)
|
|
29641
|
+
try:
|
|
29642
|
+
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0) or 0)))
|
|
29643
|
+
except Exception:
|
|
29644
|
+
body = {}
|
|
29645
|
+
pref = str(body.get("preference", "auto") or "auto").strip().lower()
|
|
29646
|
+
if pref not in PLAN_MODE_USER_CHOICES:
|
|
29647
|
+
pref = "auto"
|
|
29648
|
+
sess.plan_mode_user_preference = pref
|
|
29649
|
+
return self._send_json({"plan_mode": pref})
|
|
27333
29650
|
return self._send_json({"error": "not found"}, status=404)
|
|
27334
29651
|
|
|
27335
29652
|
def do_PATCH(self):
|