clouds-coder 2026.3.17__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.17 → clouds_coder-2026.3.20}/Clouds_Coder.py +2331 -41
- {clouds_coder-2026.3.17/clouds_coder.egg-info → clouds_coder-2026.3.20}/PKG-INFO +147 -33
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/README.md +146 -32
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20/clouds_coder.egg-info}/PKG-INFO +147 -33
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/pyproject.toml +1 -1
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/LICENSE +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.17 → clouds_coder-2026.3.20}/setup.cfg +0 -0
- {clouds_coder-2026.3.17 → 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
|
|
@@ -93,7 +93,30 @@ TRUNCATION_CONTINUATION_ECHO_CHARS = 12000
|
|
|
93
93
|
TRUNCATION_OVERLAP_SCAN_CHARS = 420
|
|
94
94
|
TRUNCATION_PAIR_SCAN_CHARS = 120_000
|
|
95
95
|
TRUNCATION_LIVE_BUFFER_MAX_CHARS = 32000
|
|
96
|
-
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
|
|
97
120
|
MAX_CONTEXT_ARCHIVE_SEGMENTS = 1200
|
|
98
121
|
MODEL_OUTPUT_RETRY_TIMES = 3
|
|
99
122
|
ARBITER_TRIGGER_MIN_CONTENT_CHARS = 50
|
|
@@ -166,21 +189,24 @@ EXECUTION_MODE_CHOICES = (
|
|
|
166
189
|
EXECUTION_MODE_SYNC,
|
|
167
190
|
)
|
|
168
191
|
AGENT_ROLES = ("explorer", "developer", "reviewer")
|
|
169
|
-
AGENT_BUBBLE_ROLES = AGENT_ROLES + ("manager",)
|
|
192
|
+
AGENT_BUBBLE_ROLES = AGENT_ROLES + ("manager", "planner")
|
|
170
193
|
AGENT_ROLE_LABELS = {
|
|
171
194
|
"explorer": "Explorer",
|
|
172
195
|
"developer": "Developer",
|
|
173
196
|
"reviewer": "Reviewer",
|
|
174
197
|
"manager": "Manager",
|
|
198
|
+
"planner": "Planner",
|
|
175
199
|
}
|
|
176
200
|
AGENT_ROLE_BUBBLE_COLORS = {
|
|
177
201
|
"explorer": "#ff5fa2",
|
|
178
202
|
"developer": "#22b455",
|
|
179
203
|
"reviewer": "#dca61b",
|
|
180
204
|
"manager": "#7c3aed",
|
|
205
|
+
"planner": "#e8533f",
|
|
181
206
|
}
|
|
182
207
|
BLACKBOARD_STATUSES = (
|
|
183
208
|
"INITIALIZING",
|
|
209
|
+
"PLANNING",
|
|
184
210
|
"RESEARCHING",
|
|
185
211
|
"CODING",
|
|
186
212
|
"TESTING",
|
|
@@ -254,6 +280,91 @@ SKILL_PROMPT_MAX_ITEMS = 40
|
|
|
254
280
|
SKILL_PROMPT_MAX_CHARS = 2600
|
|
255
281
|
SKILL_RUNTIME_CACHE_MAX_ENTRIES = 48
|
|
256
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
|
|
257
368
|
HTML_FRONTEND_REQUEST_KEYWORDS = (
|
|
258
369
|
"html",
|
|
259
370
|
"web page",
|
|
@@ -7276,6 +7387,11 @@ class SessionState:
|
|
|
7276
7387
|
self.runtime_reclassify_goal = ""
|
|
7277
7388
|
self.runtime_reclassify_required = False
|
|
7278
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"
|
|
7279
7395
|
self.rounds_without_todo = 0
|
|
7280
7396
|
self.last_todo_reminder_ts = 0.0
|
|
7281
7397
|
self.todo_reminder_count = 0
|
|
@@ -7283,6 +7399,12 @@ class SessionState:
|
|
|
7283
7399
|
self.todo_last_issue = ""
|
|
7284
7400
|
self.last_toolcall_overflow_hint_ts = 0.0
|
|
7285
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] = []
|
|
7286
7408
|
self.tool_retry_counts: dict[str, int] = {}
|
|
7287
7409
|
self.last_auto_title_ts = 0.0
|
|
7288
7410
|
self.live_thinking_text = ""
|
|
@@ -7337,6 +7459,9 @@ class SessionState:
|
|
|
7337
7459
|
self.context_archives: list[dict] = []
|
|
7338
7460
|
self.last_compact_reason = ""
|
|
7339
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}
|
|
7340
7465
|
self.created_at = now_ts()
|
|
7341
7466
|
self.updated_at = now_ts()
|
|
7342
7467
|
self.shutdown_requests: dict[str, dict] = {}
|
|
@@ -7477,6 +7602,21 @@ class SessionState:
|
|
|
7477
7602
|
used_rel=used_rel,
|
|
7478
7603
|
)
|
|
7479
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)
|
|
7480
7620
|
return items
|
|
7481
7621
|
|
|
7482
7622
|
def _latest_user_message_ts(self) -> float:
|
|
@@ -8190,8 +8330,25 @@ class SessionState:
|
|
|
8190
8330
|
}
|
|
8191
8331
|
)
|
|
8192
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
|
|
8193
8345
|
self.last_compact_reason = str(raw.get("last_compact_reason", self.last_compact_reason) or "")
|
|
8194
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)
|
|
8195
8352
|
pending_inputs = raw.get("pending_user_inputs", [])
|
|
8196
8353
|
if isinstance(pending_inputs, list):
|
|
8197
8354
|
clean_pending: list[dict] = []
|
|
@@ -8286,6 +8443,12 @@ class SessionState:
|
|
|
8286
8443
|
self.runtime_goal_reset_pending = bool(
|
|
8287
8444
|
raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
|
|
8288
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 "")
|
|
8289
8452
|
self.active_agent_role = str(raw.get("active_agent_role", self.active_agent_role) or "").strip().lower()
|
|
8290
8453
|
raw_contexts = raw.get("contexts", {})
|
|
8291
8454
|
if isinstance(raw_contexts, dict):
|
|
@@ -8400,8 +8563,13 @@ class SessionState:
|
|
|
8400
8563
|
"last_truncation_ts": self.last_truncation_ts,
|
|
8401
8564
|
"truncation_rescue_task_ids": self.truncation_rescue_task_ids[:12],
|
|
8402
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 {},
|
|
8403
8567
|
"last_compact_reason": self.last_compact_reason,
|
|
8404
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),
|
|
8405
8573
|
"pending_user_inputs": self.pending_user_inputs[-40:],
|
|
8406
8574
|
"run_generation": int(self.run_generation),
|
|
8407
8575
|
"agent_round_index": int(self.agent_round_index),
|
|
@@ -8433,6 +8601,10 @@ class SessionState:
|
|
|
8433
8601
|
"runtime_reclassify_goal": trim(str(self.runtime_reclassify_goal or ""), 4000),
|
|
8434
8602
|
"runtime_reclassify_required": bool(self.runtime_reclassify_required),
|
|
8435
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 ""),
|
|
8436
8608
|
"active_agent_role": str(self.active_agent_role or ""),
|
|
8437
8609
|
"contexts": {role: list(self.contexts.get(role, []))[-400:] for role in AGENT_ROLES},
|
|
8438
8610
|
"manager_context": list(self.manager_context)[-400:],
|
|
@@ -8580,6 +8752,7 @@ class SessionState:
|
|
|
8580
8752
|
self.runtime_reclassify_goal = ""
|
|
8581
8753
|
self.runtime_reclassify_required = False
|
|
8582
8754
|
self.runtime_goal_reset_pending = False
|
|
8755
|
+
self.runtime_plan_mode_needed = False
|
|
8583
8756
|
return removed_hints
|
|
8584
8757
|
|
|
8585
8758
|
def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
|
|
@@ -8919,6 +9092,10 @@ class SessionState:
|
|
|
8919
9092
|
"If execution stalls, load_skill('execution-degradation-recovery'). "
|
|
8920
9093
|
)
|
|
8921
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"
|
|
8922
9099
|
return (
|
|
8923
9100
|
f"You are a coding agent. Workspace: {self.files_root}. "
|
|
8924
9101
|
f"Task level={runtime_level}, mode={runtime_mode}, "
|
|
@@ -8928,6 +9105,7 @@ class SessionState:
|
|
|
8928
9105
|
"Use tools to inspect, edit, and execute. "
|
|
8929
9106
|
"Call finish_current_task when done. "
|
|
8930
9107
|
f"{skill_hint}"
|
|
9108
|
+
f"{plan_steps_block}"
|
|
8931
9109
|
f"{html_block}"
|
|
8932
9110
|
f"{research_block}"
|
|
8933
9111
|
f"{model_language_instruction(self.ui_language)}\n\n"
|
|
@@ -8936,7 +9114,28 @@ class SessionState:
|
|
|
8936
9114
|
)
|
|
8937
9115
|
|
|
8938
9116
|
def _estimate_tokens(self) -> int:
|
|
8939
|
-
|
|
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
|
|
8940
9139
|
|
|
8941
9140
|
def _context_budget_metrics(self, token_estimate: int | None = None) -> dict:
|
|
8942
9141
|
limit = max(1, int(self.context_token_upper_bound or 0))
|
|
@@ -8952,8 +9151,143 @@ class SessionState:
|
|
|
8952
9151
|
"used_percent": used_pct,
|
|
8953
9152
|
}
|
|
8954
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
|
+
|
|
8955
9278
|
def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
|
|
8956
|
-
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
|
|
8957
9291
|
metrics = self._context_budget_metrics()
|
|
8958
9292
|
used = int(metrics.get("used", 0) or 0)
|
|
8959
9293
|
limit = max(1, int(metrics.get("limit", 0) or 0))
|
|
@@ -9757,10 +10091,10 @@ class SessionState:
|
|
|
9757
10091
|
total += len(str(row.get("content", ""))) // 4
|
|
9758
10092
|
return total
|
|
9759
10093
|
|
|
9760
|
-
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]:
|
|
9761
10095
|
if not self.messages:
|
|
9762
10096
|
return []
|
|
9763
|
-
budget = max(
|
|
10097
|
+
budget = max(1000, int(token_budget))
|
|
9764
10098
|
selected: list[dict] = []
|
|
9765
10099
|
total = 0
|
|
9766
10100
|
for row in reversed(self.messages):
|
|
@@ -9810,6 +10144,60 @@ class SessionState:
|
|
|
9810
10144
|
self.context_archives = self.context_archives[-MAX_CONTEXT_ARCHIVE_SEGMENTS:]
|
|
9811
10145
|
return seg
|
|
9812
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
|
+
|
|
9813
10201
|
def _summarize_compact_rows(self, rows: list[dict]) -> str:
|
|
9814
10202
|
if not rows:
|
|
9815
10203
|
return "(no archived rows)"
|
|
@@ -9882,17 +10270,145 @@ class SessionState:
|
|
|
9882
10270
|
lines.append(f"Current focus: {focus}")
|
|
9883
10271
|
lines.append(f"Open tasks ({len(task_open)}):")
|
|
9884
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} 以来无实质进展,需要换策略。")
|
|
9885
10282
|
return "\n".join(lines)
|
|
9886
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
|
+
|
|
9887
10396
|
def _auto_compact(self, reason: str):
|
|
9888
10397
|
context_before = self._context_budget_metrics()
|
|
10398
|
+
tier = self._context_compression_tier(context_before)
|
|
9889
10399
|
transcript_dir = self.root / "transcripts"
|
|
9890
10400
|
transcript_dir.mkdir(parents=True, exist_ok=True)
|
|
9891
10401
|
transcript_path = transcript_dir / f"transcript_{int(now_ts())}.jsonl"
|
|
9892
10402
|
with transcript_path.open("w", encoding="utf-8") as handle:
|
|
9893
10403
|
for msg in self.messages:
|
|
9894
10404
|
handle.write(json_dumps(msg) + "\n")
|
|
9895
|
-
|
|
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)))
|
|
9896
10412
|
tail = self._select_compact_tail(tail_budget)
|
|
9897
10413
|
if len(tail) >= len(self.messages):
|
|
9898
10414
|
tail = self._select_compact_tail(max(2200, int(tail_budget * 0.55)), min_count=4, max_count=20)
|
|
@@ -9907,35 +10423,59 @@ class SessionState:
|
|
|
9907
10423
|
if seg_id
|
|
9908
10424
|
else "If details are missing, call context_recall with recent_segments=2."
|
|
9909
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 ""
|
|
9910
10432
|
compact_note = (
|
|
9911
10433
|
"<compact-resume>\n"
|
|
9912
10434
|
f"reason: {reason}\n"
|
|
10435
|
+
f"tier: {tier}\n"
|
|
9913
10436
|
f"transcript: {transcript_path}\n"
|
|
9914
10437
|
f"archive_segment: {seg_id or 'none'} messages={seg_msg_count} path={seg_path or '-'}\n"
|
|
10438
|
+
f"{state_handoff}\n"
|
|
9915
10439
|
f"{self._open_work_brief()}\n"
|
|
10440
|
+
f"{failure_section}"
|
|
9916
10441
|
f"summary:\n{summary}\n"
|
|
9917
10442
|
f"{continuation}\n"
|
|
9918
10443
|
"Continue exploring from current pending work; do not restart from scratch.\n"
|
|
9919
10444
|
"</compact-resume>"
|
|
9920
10445
|
)
|
|
9921
10446
|
tail.append({"role": "user", "content": compact_note, "ts": now_ts()})
|
|
9922
|
-
|
|
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)))
|
|
9923
10454
|
while len(tail) > 5 and self._estimate_messages_tokens(tail) > target_tokens:
|
|
9924
10455
|
tail.pop(0)
|
|
9925
10456
|
if self._estimate_messages_tokens(tail) > target_tokens:
|
|
9926
10457
|
for msg in tail:
|
|
9927
10458
|
if msg.get("role") == "tool":
|
|
9928
|
-
msg["content"] = trim(msg.get("content", ""), 1800)
|
|
10459
|
+
msg["content"] = trim(msg.get("content", ""), 1800 if tier < 2 else 600)
|
|
9929
10460
|
while len(tail) > 2 and self._estimate_messages_tokens(tail) > target_tokens:
|
|
9930
10461
|
tail.pop(0)
|
|
9931
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")
|
|
9932
10471
|
self.last_compact_reason = str(reason or "")
|
|
9933
10472
|
self.last_compact_ts = now_ts()
|
|
9934
10473
|
self._emit(
|
|
9935
10474
|
"compact",
|
|
9936
10475
|
{
|
|
9937
|
-
"summary": f"context compacted ({reason}) archives={len(self.context_archives)}",
|
|
10476
|
+
"summary": f"context compacted ({reason}) tier={tier} archives={len(self.context_archives)}",
|
|
9938
10477
|
"reason": str(reason or ""),
|
|
10478
|
+
"tier": tier,
|
|
9939
10479
|
"archive_segment": seg_id,
|
|
9940
10480
|
"archived_messages": seg_msg_count,
|
|
9941
10481
|
"context_limit_before": int(context_before.get("limit", 0)),
|
|
@@ -9945,6 +10485,50 @@ class SessionState:
|
|
|
9945
10485
|
},
|
|
9946
10486
|
)
|
|
9947
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
|
+
|
|
9948
10532
|
def _git_status_map(self, cwd: Path) -> dict[str, str]:
|
|
9949
10533
|
try:
|
|
9950
10534
|
check = subprocess.run(
|
|
@@ -11303,7 +11887,7 @@ class SessionState:
|
|
|
11303
11887
|
active_rows.append(trim(content, 140))
|
|
11304
11888
|
if len(active_rows) >= 3:
|
|
11305
11889
|
break
|
|
11306
|
-
|
|
11890
|
+
snapshot = {
|
|
11307
11891
|
"goal": self._latest_user_goal_text(),
|
|
11308
11892
|
"todo_open_count": int(open_count),
|
|
11309
11893
|
"todo_in_progress_count": int(in_progress_count),
|
|
@@ -11312,6 +11896,20 @@ class SessionState:
|
|
|
11312
11896
|
"assistant_thinking": trim(str(thinking_text or "").strip(), 700),
|
|
11313
11897
|
"agent_round_index": int(self.agent_round_index),
|
|
11314
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
|
|
11315
11913
|
|
|
11316
11914
|
def _build_arbiter_client(self) -> OllamaClient:
|
|
11317
11915
|
client = OllamaClient(self.ollama.base_url, self.ollama.model)
|
|
@@ -11412,16 +12010,41 @@ class SessionState:
|
|
|
11412
12010
|
|
|
11413
12011
|
def _mark_all_done_silently(self, reason: str = "") -> dict:
|
|
11414
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
|
+
}
|
|
11415
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")
|
|
11416
12038
|
updated = int(result.get("updated", 0) or 0)
|
|
11417
12039
|
total = int(result.get("total", 0) or 0)
|
|
12040
|
+
pending_steps = len(saved_plan_steps)
|
|
11418
12041
|
self._prune_runtime_retry_hints()
|
|
11419
12042
|
self._emit(
|
|
11420
12043
|
"status",
|
|
11421
12044
|
{
|
|
11422
12045
|
"summary": (
|
|
11423
|
-
"arbiter marked task completed; "
|
|
11424
|
-
f"
|
|
12046
|
+
f"arbiter marked task completed; todos completed={updated}/{total}"
|
|
12047
|
+
+ (f"; {pending_steps} plan steps protected (still pending)" if pending_steps else "")
|
|
11425
12048
|
)
|
|
11426
12049
|
},
|
|
11427
12050
|
)
|
|
@@ -12501,13 +13124,61 @@ class SessionState:
|
|
|
12501
13124
|
def _run_read(self, path: str, limit: int | None = None) -> str:
|
|
12502
13125
|
try:
|
|
12503
13126
|
rel = self._normalize_tool_path_text(path)
|
|
12504
|
-
|
|
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()
|
|
12505
13137
|
if limit and limit < len(lines):
|
|
12506
13138
|
lines = lines[:limit] + [f"... ({len(lines) - limit} more)"]
|
|
12507
13139
|
return trim("\n".join(lines))
|
|
12508
13140
|
except Exception as exc:
|
|
12509
13141
|
return f"Error: {type(exc).__name__}: {exc}"
|
|
12510
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
|
+
|
|
12511
13182
|
def _is_html_file_rel(self, path: str) -> bool:
|
|
12512
13183
|
low = str(path or "").strip().lower()
|
|
12513
13184
|
return low.endswith(".html") or low.endswith(".htm")
|
|
@@ -13313,6 +13984,15 @@ class SessionState:
|
|
|
13313
13984
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
13314
13985
|
if bool(self.runtime_goal_reset_pending):
|
|
13315
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)}"
|
|
13316
13996
|
# Project todo gate: coding tasks must pass compile + test
|
|
13317
13997
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
13318
13998
|
task_type = str(profile.get("task_type", "general") or "general")
|
|
@@ -13371,8 +14051,16 @@ class SessionState:
|
|
|
13371
14051
|
|
|
13372
14052
|
def _manager_progress_state(self, board: dict | None = None) -> str:
|
|
13373
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
|
+
)
|
|
13374
14062
|
can_finish, _ = self._can_auto_finish_from_approval(bb)
|
|
13375
|
-
if can_finish:
|
|
14063
|
+
if can_finish and not has_pending_plan_steps:
|
|
13376
14064
|
return "done"
|
|
13377
14065
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
13378
14066
|
task_type = str(profile.get("task_type", "") or "")
|
|
@@ -14053,6 +14741,8 @@ class SessionState:
|
|
|
14053
14741
|
bb["watchdog"] = wd
|
|
14054
14742
|
self.blackboard = bb
|
|
14055
14743
|
self._blackboard_touch()
|
|
14744
|
+
# Failure ledger: record stall event
|
|
14745
|
+
self._ledger_record_stall(trigger_reason, "unresolved")
|
|
14056
14746
|
if int(wd["trigger_count"]) >= 2:
|
|
14057
14747
|
self._watchdog_escalate_to_single_developer(bb, reason=trigger_reason)
|
|
14058
14748
|
else:
|
|
@@ -14200,6 +14890,16 @@ class SessionState:
|
|
|
14200
14890
|
"project_todos": [],
|
|
14201
14891
|
"watchdog": self._new_watchdog_state(),
|
|
14202
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": [],
|
|
14203
14903
|
}
|
|
14204
14904
|
|
|
14205
14905
|
def _normalize_blackboard(self, raw: object) -> dict:
|
|
@@ -14371,8 +15071,324 @@ class SessionState:
|
|
|
14371
15071
|
board["decomposition_queue"] = self._normalize_decomposition_queue_state(
|
|
14372
15072
|
src.get("decomposition_queue", {})
|
|
14373
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 []
|
|
14374
15092
|
return board
|
|
14375
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
|
+
|
|
14376
15392
|
def _ensure_blackboard(self) -> dict:
|
|
14377
15393
|
if not isinstance(self.blackboard, dict) or not self.blackboard:
|
|
14378
15394
|
self.blackboard = self._new_blackboard(self._latest_user_goal_text())
|
|
@@ -14729,6 +15745,31 @@ class SessionState:
|
|
|
14729
15745
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14730
15746
|
if bb.get("project_todos"):
|
|
14731
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
|
|
14732
15773
|
raw = self._generate_project_todos_from_profile(bb)
|
|
14733
15774
|
bb["project_todos"] = [
|
|
14734
15775
|
{
|
|
@@ -14806,6 +15847,23 @@ class SessionState:
|
|
|
14806
15847
|
todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
|
|
14807
15848
|
elif cat == "review" and feedback_pass:
|
|
14808
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"
|
|
14809
15867
|
|
|
14810
15868
|
if not any(t.get("status") == "in_progress" for t in todos):
|
|
14811
15869
|
for t in todos:
|
|
@@ -14816,6 +15874,104 @@ class SessionState:
|
|
|
14816
15874
|
bb["project_todos"] = todos
|
|
14817
15875
|
self.blackboard = bb
|
|
14818
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
|
+
|
|
14819
15975
|
def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
|
|
14820
15976
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
14821
15977
|
todos = bb.get("project_todos", [])
|
|
@@ -14881,6 +16037,42 @@ class SessionState:
|
|
|
14881
16037
|
self._blackboard_touch()
|
|
14882
16038
|
self._sync_todos_from_blackboard(reason="approved", board=board)
|
|
14883
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
|
+
|
|
14884
16076
|
def _auto_summary_on_finish(self) -> str:
|
|
14885
16077
|
"""Generate concise summary from blackboard state when run ends."""
|
|
14886
16078
|
bb = self._ensure_blackboard()
|
|
@@ -15035,7 +16227,9 @@ class SessionState:
|
|
|
15035
16227
|
|
|
15036
16228
|
proj_todos = board.get("project_todos", [])
|
|
15037
16229
|
if proj_todos:
|
|
15038
|
-
|
|
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}")
|
|
15039
16233
|
for pt in proj_todos:
|
|
15040
16234
|
s = pt.get("status", "pending")
|
|
15041
16235
|
c = trim(str(pt.get("content", "") or ""), 200)
|
|
@@ -15046,8 +16240,9 @@ class SessionState:
|
|
|
15046
16240
|
mark = "[>]"
|
|
15047
16241
|
else:
|
|
15048
16242
|
mark = "[ ]"
|
|
16243
|
+
prefix = f"Step {int(pt.get('plan_step_index', 0) or 0) + 1}: " if has_plan_steps else ""
|
|
15049
16244
|
suffix = f" — {ev}" if ev else ""
|
|
15050
|
-
lines.append(f"- {mark} {c}{suffix}")
|
|
16245
|
+
lines.append(f"- {mark} {prefix}{c}{suffix}")
|
|
15051
16246
|
|
|
15052
16247
|
return "\n".join(lines)
|
|
15053
16248
|
|
|
@@ -15072,6 +16267,10 @@ class SessionState:
|
|
|
15072
16267
|
"requires_user_confirmation": {"type": "boolean"},
|
|
15073
16268
|
"is_mandatory": {"type": "boolean"},
|
|
15074
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
|
+
},
|
|
15075
16274
|
},
|
|
15076
16275
|
["target", "instruction"],
|
|
15077
16276
|
)
|
|
@@ -15100,6 +16299,15 @@ class SessionState:
|
|
|
15100
16299
|
"participants": {"type": "array", "items": {"type": "string", "enum": list(AGENT_ROLES)}},
|
|
15101
16300
|
"assigned_expert": {"type": "string", "enum": list(AGENT_ROLES)},
|
|
15102
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
|
+
},
|
|
15103
16311
|
},
|
|
15104
16312
|
["level", "judgement", "inherit_previous_state"],
|
|
15105
16313
|
)
|
|
@@ -15373,6 +16581,11 @@ class SessionState:
|
|
|
15373
16581
|
level = 3
|
|
15374
16582
|
if level not in TASK_LEVEL_CHOICES:
|
|
15375
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
|
|
15376
16589
|
policy = dict(TASK_LEVEL_POLICIES.get(level, TASK_LEVEL_POLICIES[3]))
|
|
15377
16590
|
policy_mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
|
|
15378
16591
|
config_mode = normalize_execution_mode(self.execution_mode, default="")
|
|
@@ -15505,7 +16718,9 @@ class SessionState:
|
|
|
15505
16718
|
objective = trim(str(row.get("direct_objective", "") or "").strip(), 800)
|
|
15506
16719
|
if not objective:
|
|
15507
16720
|
objective = fallback_objective
|
|
16721
|
+
_prev_level_val = int(getattr(self, '_prev_applied_task_level', 0) or 0)
|
|
15508
16722
|
self.runtime_task_level = int(level)
|
|
16723
|
+
self._prev_applied_task_level = int(level)
|
|
15509
16724
|
self.runtime_execution_mode = mode
|
|
15510
16725
|
self.runtime_assigned_expert = assigned
|
|
15511
16726
|
self.runtime_participants = list(participants)
|
|
@@ -15519,6 +16734,32 @@ class SessionState:
|
|
|
15519
16734
|
self.runtime_direct_objective = objective
|
|
15520
16735
|
self.runtime_reclassify_goal = trim(str(goal_text or "").strip(), 4000)
|
|
15521
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)
|
|
15522
16763
|
board = self._ensure_blackboard()
|
|
15523
16764
|
profile = self._ensure_blackboard_task_profile(board)
|
|
15524
16765
|
profile["task_level"] = int(level)
|
|
@@ -15568,6 +16809,13 @@ class SessionState:
|
|
|
15568
16809
|
)
|
|
15569
16810
|
},
|
|
15570
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
|
+
})
|
|
15571
16819
|
|
|
15572
16820
|
def _manager_classify_task_level(
|
|
15573
16821
|
self,
|
|
@@ -15599,6 +16847,15 @@ class SessionState:
|
|
|
15599
16847
|
"IMPORTANT: User has configured Single-agent mode. "
|
|
15600
16848
|
"Prefer level 1-2 for simple tasks. Only escalate to level 3+ if truly complex.\n"
|
|
15601
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
|
+
)
|
|
15602
16859
|
with self.lock:
|
|
15603
16860
|
self.current_phase = "manager:classify:model-call"
|
|
15604
16861
|
self.current_tool_name = ""
|
|
@@ -15644,6 +16901,61 @@ class SessionState:
|
|
|
15644
16901
|
row["low_confidence_reason"] = "manager classifier returned no valid tool call"
|
|
15645
16902
|
return row
|
|
15646
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
|
+
|
|
15647
16959
|
def _refresh_runtime_task_policy(
|
|
15648
16960
|
self,
|
|
15649
16961
|
*,
|
|
@@ -15655,6 +16967,26 @@ class SessionState:
|
|
|
15655
16967
|
goal = trim(str(goal_text or self.runtime_reclassify_goal or self._latest_user_goal_text() or "").strip(), 4000)
|
|
15656
16968
|
if not goal:
|
|
15657
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)
|
|
15658
16990
|
need = bool(
|
|
15659
16991
|
force
|
|
15660
16992
|
or self.runtime_reclassify_required
|
|
@@ -15681,11 +17013,94 @@ class SessionState:
|
|
|
15681
17013
|
self._apply_runtime_task_decision(goal, decision)
|
|
15682
17014
|
return dict(decision or {})
|
|
15683
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
|
+
|
|
15684
17082
|
def _project_todo_hint_for_manager(self) -> str:
|
|
15685
17083
|
bb = self._ensure_blackboard()
|
|
15686
17084
|
todos = bb.get("project_todos", [])
|
|
15687
17085
|
if not todos:
|
|
15688
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
|
+
# 原有逻辑
|
|
15689
17104
|
pending = [t for t in todos if t.get("status") != "completed"]
|
|
15690
17105
|
if not pending:
|
|
15691
17106
|
return "All project tasks completed. Route to finish. "
|
|
@@ -15713,6 +17128,35 @@ class SessionState:
|
|
|
15713
17128
|
"Explorer should only be used for specific file/API lookups, not broad analysis. "
|
|
15714
17129
|
)
|
|
15715
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
|
+
)
|
|
15716
17160
|
return (
|
|
15717
17161
|
"You are Manager in a multi-agent coding system. "
|
|
15718
17162
|
"Read blackboard, delegate one short timeslice via route_to_next_agent. "
|
|
@@ -15721,10 +17165,13 @@ class SessionState:
|
|
|
15721
17165
|
"Role capabilities: "
|
|
15722
17166
|
"Explorer=read-only (bash/read_file/search/blackboard, NO write_file/edit_file); "
|
|
15723
17167
|
"Developer=all tools (write_file/edit_file/bash/read_file/etc); "
|
|
15724
|
-
"Reviewer=read+verify (bash/read_file/finish_task, NO write_file/edit_file). "
|
|
15725
|
-
"NEVER delegate file-writing tasks to Explorer or Reviewer. "
|
|
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. '}"
|
|
15726
17170
|
f"{coding_hint}"
|
|
15727
17171
|
f"{project_todo_hint}"
|
|
17172
|
+
f"{plan_context}"
|
|
17173
|
+
f"{phase_hint}"
|
|
17174
|
+
f"{failure_hint}"
|
|
15728
17175
|
f"Level={level}, mode={mode}, progress={progress}, "
|
|
15729
17176
|
f"budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
|
|
15730
17177
|
f"objective={trim(str(profile.get('direct_objective','') or ''), 220)}. "
|
|
@@ -15891,7 +17338,25 @@ class SessionState:
|
|
|
15891
17338
|
cycles = int(board.get("manager_cycles", 0) or 0)
|
|
15892
17339
|
summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
|
|
15893
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
|
+
]
|
|
15894
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
|
+
}
|
|
15895
17360
|
self._emit("status", {"summary": "Max cycles reached; forcing finish."})
|
|
15896
17361
|
return {
|
|
15897
17362
|
"target": "finish",
|
|
@@ -16167,12 +17632,43 @@ class SessionState:
|
|
|
16167
17632
|
return row
|
|
16168
17633
|
target = str(row.get("target", "") or "").strip().lower()
|
|
16169
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
|
|
16170
17666
|
if task_type_low in ("simple_code", "engineering") and target == "explorer":
|
|
16171
|
-
board =
|
|
17667
|
+
board = bb_for_routes
|
|
16172
17668
|
progress = self._manager_progress_state(board)
|
|
16173
17669
|
if progress in ("initializing", "in_progress"):
|
|
16174
17670
|
explorer_count = sum(
|
|
16175
|
-
1 for x in
|
|
17671
|
+
1 for x in merged_routes[-8:]
|
|
16176
17672
|
if str(x.get("target", "") or "").strip().lower() == "explorer"
|
|
16177
17673
|
)
|
|
16178
17674
|
if explorer_count >= 2:
|
|
@@ -16186,9 +17682,9 @@ class SessionState:
|
|
|
16186
17682
|
return row
|
|
16187
17683
|
if target not in AGENT_ROLES:
|
|
16188
17684
|
return row
|
|
16189
|
-
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:]]
|
|
16190
17686
|
if len(recent) >= 2 and recent[-1] == target and recent[-2] == target:
|
|
16191
|
-
board =
|
|
17687
|
+
board = bb_for_routes
|
|
16192
17688
|
low_reason = str(row.get("reason", "") or "").strip().lower()
|
|
16193
17689
|
if "summary" in low_reason and len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16194
17690
|
row["target"] = "finish"
|
|
@@ -16211,7 +17707,7 @@ class SessionState:
|
|
|
16211
17707
|
row["source"] = "anti-stall"
|
|
16212
17708
|
return row
|
|
16213
17709
|
if len(recent) == 4 and recent[0] == recent[2] and recent[1] == recent[3] and recent[0] != recent[1]:
|
|
16214
|
-
board =
|
|
17710
|
+
board = bb_for_routes
|
|
16215
17711
|
if len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16216
17712
|
row["target"] = "finish"
|
|
16217
17713
|
row["instruction"] = "Oscillation detected with existing outputs; finish now."
|
|
@@ -16402,7 +17898,17 @@ class SessionState:
|
|
|
16402
17898
|
target = assigned_expert
|
|
16403
17899
|
else:
|
|
16404
17900
|
if self._manager_has_error_log(board):
|
|
16405
|
-
|
|
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"
|
|
16406
17912
|
elif len(board.get("code_artifacts", {}) or {}) > 0:
|
|
16407
17913
|
target = "reviewer"
|
|
16408
17914
|
elif len(board.get("research_notes", []) or []) > 0:
|
|
@@ -16582,6 +18088,7 @@ class SessionState:
|
|
|
16582
18088
|
"requires_user_confirmation": bool(args.get("requires_user_confirmation", False)),
|
|
16583
18089
|
"is_mandatory": _to_bool_like(args.get("is_mandatory", False), default=False),
|
|
16584
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),
|
|
16585
18092
|
"round_budget": args.get("round_budget", 0),
|
|
16586
18093
|
"reason": trim(str(text or "").strip(), 600),
|
|
16587
18094
|
"source": "tool",
|
|
@@ -16878,6 +18385,29 @@ class SessionState:
|
|
|
16878
18385
|
active_profile = self._ensure_blackboard_task_profile(board)
|
|
16879
18386
|
target = str(route.get("target", "") or "").strip().lower()
|
|
16880
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}"
|
|
16881
18411
|
try:
|
|
16882
18412
|
task_level = int(route.get("task_level", active_profile.get("task_level", self.runtime_task_level or 3)) or 3)
|
|
16883
18413
|
except Exception:
|
|
@@ -16945,8 +18475,27 @@ class SessionState:
|
|
|
16945
18475
|
"round_budget": int(round_budget),
|
|
16946
18476
|
"remaining_rounds": int(remaining_rounds),
|
|
16947
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
|
+
)
|
|
16948
18484
|
self.manager_routes.append(route_row)
|
|
16949
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)
|
|
16950
18499
|
profile = self._ensure_blackboard_task_profile(board)
|
|
16951
18500
|
profile["task_level"] = int(task_level)
|
|
16952
18501
|
profile["execution_mode"] = execution_mode
|
|
@@ -17109,7 +18658,23 @@ class SessionState:
|
|
|
17109
18658
|
role_capability_note = {
|
|
17110
18659
|
"explorer": "YOUR TOOLS: read-only (bash/read_file/search/blackboard). You CANNOT write_file or edit_file.",
|
|
17111
18660
|
"developer": "YOUR TOOLS: all tools available (write_file/edit_file/bash/read_file/etc).",
|
|
17112
|
-
"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
|
+
),
|
|
17113
18678
|
}.get(role_key, "")
|
|
17114
18679
|
if role_key == "explorer":
|
|
17115
18680
|
tool_examples = "bash/read_file/read_from_blackboard"
|
|
@@ -17140,6 +18705,16 @@ class SessionState:
|
|
|
17140
18705
|
"use ask_colleague immediately with explicit intent and concise payload; "
|
|
17141
18706
|
"do not wait for another manager cycle."
|
|
17142
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
|
+
)
|
|
17143
18718
|
board_md = self._blackboard_read_state_markdown(max_items=5)
|
|
17144
18719
|
payload = (
|
|
17145
18720
|
"<manager-delegate>\n"
|
|
@@ -17152,6 +18727,7 @@ class SessionState:
|
|
|
17152
18727
|
f"{executor_note}\n"
|
|
17153
18728
|
f"{collaboration_note}\n"
|
|
17154
18729
|
f"{role_capability_note}\n"
|
|
18730
|
+
f"{error_section}"
|
|
17155
18731
|
"</manager-delegate>\n"
|
|
17156
18732
|
"<blackboard-state>\n"
|
|
17157
18733
|
f"{trim(board_md, 6000)}\n"
|
|
@@ -17680,10 +19256,13 @@ class SessionState:
|
|
|
17680
19256
|
row["agent_role"] = role_key
|
|
17681
19257
|
if "ts" not in row:
|
|
17682
19258
|
row["ts"] = now_ts()
|
|
17683
|
-
# Write to unified agent_messages
|
|
19259
|
+
# Write to unified agent_messages with tier-aware trim
|
|
17684
19260
|
self.agent_messages.append(row)
|
|
17685
|
-
|
|
17686
|
-
|
|
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:]
|
|
17687
19266
|
if mirror_to_global:
|
|
17688
19267
|
mirror = dict(row)
|
|
17689
19268
|
if "tool_calls" in mirror and isinstance(mirror.get("tool_calls"), list):
|
|
@@ -17711,10 +19290,14 @@ class SessionState:
|
|
|
17711
19290
|
if "ts" not in row:
|
|
17712
19291
|
row["ts"] = now_ts()
|
|
17713
19292
|
self.manager_context.append(row)
|
|
17714
|
-
|
|
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:]
|
|
17715
19297
|
self.agent_messages.append(row)
|
|
17716
|
-
|
|
17717
|
-
|
|
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:]
|
|
17718
19301
|
|
|
17719
19302
|
def _agent_display_name(self, role: str) -> str:
|
|
17720
19303
|
return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
|
|
@@ -17869,12 +19452,41 @@ class SessionState:
|
|
|
17869
19452
|
"Use blackboard for shared state, ask_colleague for inter-agent communication. "
|
|
17870
19453
|
"Keep outputs concise and action-oriented. "
|
|
17871
19454
|
"Use load_skill for detailed guidance (multi-agent-guide, code-review-checklist, finish-protocol). "
|
|
19455
|
+
f"{_detect_os_shell_instruction()} "
|
|
17872
19456
|
f"{model_language_instruction(self.ui_language)} "
|
|
17873
19457
|
)
|
|
17874
19458
|
if role_key == "explorer":
|
|
17875
19459
|
return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
|
|
17876
19460
|
if role_key == "reviewer":
|
|
17877
|
-
|
|
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
|
+
)
|
|
17878
19490
|
return base + "Role: implement code changes, execute tools, record progress to blackboard. "
|
|
17879
19491
|
|
|
17880
19492
|
def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
|
|
@@ -18686,6 +20298,20 @@ class SessionState:
|
|
|
18686
20298
|
)
|
|
18687
20299
|
return out
|
|
18688
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)
|
|
18689
20315
|
return self.todo.update(args["items"])
|
|
18690
20316
|
if name == "TodoWriteRescue":
|
|
18691
20317
|
return self._todo_write_rescue(args)
|
|
@@ -18726,6 +20352,9 @@ class SessionState:
|
|
|
18726
20352
|
todo_mark = self.todo.complete_all_open(summary)
|
|
18727
20353
|
else:
|
|
18728
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")
|
|
18729
20358
|
updated = int(todo_mark.get("updated", 0) or 0)
|
|
18730
20359
|
if updated > 0:
|
|
18731
20360
|
self._emit(
|
|
@@ -18838,6 +20467,25 @@ class SessionState:
|
|
|
18838
20467
|
return "Error: ask_colleague.intent is required"
|
|
18839
20468
|
if not content:
|
|
18840
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
|
+
)
|
|
18841
20489
|
env = self._agent_send_bus(from_role, to_role, intent, content)
|
|
18842
20490
|
return (
|
|
18843
20491
|
f"agent_bus sent ({env.get('from')} -> {env.get('to')}, "
|
|
@@ -19049,6 +20697,8 @@ class SessionState:
|
|
|
19049
20697
|
"</live-user-adjustment>"
|
|
19050
20698
|
)
|
|
19051
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)
|
|
19052
20702
|
self.runtime_reclassify_goal = trim(content, 4000)
|
|
19053
20703
|
self.runtime_reclassify_required = True
|
|
19054
20704
|
self.runtime_goal_reset_pending = True
|
|
@@ -19082,6 +20732,104 @@ class SessionState:
|
|
|
19082
20732
|
)
|
|
19083
20733
|
return len(injected)
|
|
19084
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
|
+
|
|
19085
20833
|
def submit_user_message(self, content: str):
|
|
19086
20834
|
start_worker = False
|
|
19087
20835
|
dropped_stale_inputs = 0
|
|
@@ -19092,12 +20840,37 @@ class SessionState:
|
|
|
19092
20840
|
else:
|
|
19093
20841
|
if self.pending_user_inputs:
|
|
19094
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
|
+
)
|
|
19095
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
|
|
19096
20852
|
self.run_generation = int(self.run_generation) + 1
|
|
19097
20853
|
clean_goal = trim(str(content or "").strip(), 4000)
|
|
19098
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)
|
|
19099
20868
|
self.runtime_reclassify_goal = clean_goal
|
|
19100
|
-
|
|
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
|
|
19101
20874
|
self.runtime_goal_reset_pending = True
|
|
19102
20875
|
self.running = True
|
|
19103
20876
|
self.current_phase = "starting"
|
|
@@ -19380,6 +21153,25 @@ class SessionState:
|
|
|
19380
21153
|
}
|
|
19381
21154
|
# Atomic blackboard sync: update shared state immediately after each tool result.
|
|
19382
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
|
+
)
|
|
19383
21175
|
item["bb_applied"] = True
|
|
19384
21176
|
tool_results.append(item)
|
|
19385
21177
|
if name in {"finish_task", "finish_current_task", "mark_done"} and (not str(output).startswith("Error:")):
|
|
@@ -19568,6 +21360,9 @@ class SessionState:
|
|
|
19568
21360
|
self._emit("status", {"summary": "run interrupted"})
|
|
19569
21361
|
break
|
|
19570
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()
|
|
19571
21366
|
with self.lock:
|
|
19572
21367
|
self.agent_round_index = int(self.agent_round_index) + 1
|
|
19573
21368
|
self.current_phase = "manager:dispatch"
|
|
@@ -19910,6 +21705,450 @@ class SessionState:
|
|
|
19910
21705
|
self._mark_all_done_silently(f"budget exhausted: {summary}")
|
|
19911
21706
|
self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
|
|
19912
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
|
+
|
|
19913
22152
|
def _agent_worker(self):
|
|
19914
22153
|
single_role = "developer"
|
|
19915
22154
|
try:
|
|
@@ -19960,6 +22199,10 @@ class SessionState:
|
|
|
19960
22199
|
{"summary": "level-5 requires user confirmation before next actions"},
|
|
19961
22200
|
)
|
|
19962
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
|
|
19963
22206
|
if self._is_multi_agent_mode():
|
|
19964
22207
|
self._seed_multi_agent_contexts_if_needed(self.runtime_reclassify_goal or "")
|
|
19965
22208
|
self._emit(
|
|
@@ -20044,6 +22287,10 @@ class SessionState:
|
|
|
20044
22287
|
)
|
|
20045
22288
|
break
|
|
20046
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()
|
|
20047
22294
|
notifs = self.bg.drain()
|
|
20048
22295
|
if notifs:
|
|
20049
22296
|
text = "\n".join(f"[bg:{n['task_id']}] {n['status']}: {n['result']}" for n in notifs)
|
|
@@ -20783,6 +23030,25 @@ class SessionState:
|
|
|
20783
23030
|
"ok": not str(output).startswith("Error:"),
|
|
20784
23031
|
}
|
|
20785
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
|
+
)
|
|
20786
23052
|
self._emit("tool_result", {"name": name, "result": trim(output, 500), "summary": f"tool done: {name}"})
|
|
20787
23053
|
if int(tool_error_streaks.get(tool_key, 0) or 0) >= HARD_BREAK_TOOL_ERROR_THRESHOLD:
|
|
20788
23054
|
stop_due_to_hard_break = True
|
|
@@ -20823,6 +23089,8 @@ class SessionState:
|
|
|
20823
23089
|
state_changed=bool(single_watchdog_after_fp != single_watchdog_before_fp),
|
|
20824
23090
|
pinned_selection=pinned_selection,
|
|
20825
23091
|
)
|
|
23092
|
+
# Single-agent plan step tracking: sync todos and auto-advance
|
|
23093
|
+
self._single_agent_plan_step_check(single_round_tool_results)
|
|
20826
23094
|
if stop_due_to_hard_break:
|
|
20827
23095
|
note = (
|
|
20828
23096
|
"Execution paused after repeated tool/recovery failures. "
|
|
@@ -21345,6 +23613,7 @@ class SessionState:
|
|
|
21345
23613
|
"context_token_upper_bound": int(self.context_token_upper_bound),
|
|
21346
23614
|
"context_token_limit_config": int(self.max_context_token_limit),
|
|
21347
23615
|
"context_token_limit_locked": bool(self.context_limit_locked),
|
|
23616
|
+
"plan_mode_preference": str(self.plan_mode_user_preference or "auto"),
|
|
21348
23617
|
"context_tokens_estimate": int(ctx.get("used", 0)),
|
|
21349
23618
|
"context_left_tokens": int(ctx.get("left", 0)),
|
|
21350
23619
|
"context_left_percent": round(float(ctx.get("left_percent", 0.0)), 2),
|
|
@@ -22367,6 +24636,7 @@ window.MathJax={
|
|
|
22367
24636
|
<button id="sendBtn">Send</button>
|
|
22368
24637
|
<button id="interruptBtn" class="subtle">Interrupt</button>
|
|
22369
24638
|
<button id="compactBtn" class="subtle">Compact</button>
|
|
24639
|
+
<button id="planModeBtn" class="subtle" title="Toggle Plan Mode">Plan: Auto</button>
|
|
22370
24640
|
<button id="refreshBtn" class="subtle">Refresh</button>
|
|
22371
24641
|
<div class="export-dropdown" style="position:relative;display:inline-block">
|
|
22372
24642
|
<button id="exportMenuBtn" class="subtle">Export ▾</button>
|
|
@@ -22524,11 +24794,13 @@ main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px)
|
|
|
22524
24794
|
.msg.assistant.agent-developer,.msg.system.agent-developer{background:#e9fbea;border:1px solid #bde6c4}
|
|
22525
24795
|
.msg.assistant.agent-reviewer,.msg.system.agent-reviewer{background:#fff6de;border:1px solid #f1d89a}
|
|
22526
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}
|
|
22527
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}
|
|
22528
24799
|
.msg-agent-badge.explorer{background:#ffe6f0;border-color:#ffbdd5;color:#9b275e}
|
|
22529
24800
|
.msg-agent-badge.developer{background:#e6f8e8;border-color:#b8e2c0;color:#1c6a33}
|
|
22530
24801
|
.msg-agent-badge.reviewer{background:#fff3d4;border-color:#efd692;color:#8a6213}
|
|
22531
24802
|
.msg-agent-badge.manager{background:#efe4ff;border-color:#d2b6ff;color:#6e36b8}
|
|
24803
|
+
.msg-agent-badge.planner{background:#fde8e4;border-color:#f5b8ad;color:#c0392b}
|
|
22532
24804
|
.manager-delegate-card{border:1px solid #d7bbff;background:#f8f3ff;border-radius:10px;padding:10px}
|
|
22533
24805
|
.manager-delegate-head{font-weight:800;font-size:.86rem;color:#5a2fa6}
|
|
22534
24806
|
.manager-delegate-route{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0 8px}
|
|
@@ -23490,6 +25762,8 @@ self.onmessage=(ev)=>{
|
|
|
23490
25762
|
slot.classList.remove('md-async-slot');
|
|
23491
25763
|
const msg=slot.closest('.msg');
|
|
23492
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}}}
|
|
23493
25767
|
const req=String(msg.getAttribute('data-math-request')||'').trim();
|
|
23494
25768
|
if(req)_mathTypeset(msg,`chat:${req}`);
|
|
23495
25769
|
}else{
|
|
@@ -24169,7 +26443,7 @@ function _chatVirtCollectRows(){
|
|
|
24169
26443
|
const label=String(S.snap?.live_run_notice_label||'model call').trim()||'model call';
|
|
24170
26444
|
const startedAt=Number(S.snap?.live_run_notice_started_at||0);
|
|
24171
26445
|
const snapElapsed=Number(S.snap?.live_run_notice_elapsed||0);
|
|
24172
|
-
const elapsed=startedAt>0?
|
|
26446
|
+
const elapsed=startedAt>0?(Date.now()/1000)-startedAt:snapElapsed;
|
|
24173
26447
|
rows.push({
|
|
24174
26448
|
role:'assistant',
|
|
24175
26449
|
type:'live_run_notice',
|
|
@@ -24178,12 +26452,12 @@ function _chatVirtCollectRows(){
|
|
|
24178
26452
|
elapsed:elapsed,
|
|
24179
26453
|
startedAt:startedAt,
|
|
24180
26454
|
agent_role:activeAgentRole||undefined,
|
|
24181
|
-
_vk:`run:${activeAgentRole}:${label}
|
|
26455
|
+
_vk:`run:${activeAgentRole}:${label}`
|
|
24182
26456
|
});
|
|
24183
26457
|
}
|
|
24184
26458
|
return rows;
|
|
24185
26459
|
}
|
|
24186
|
-
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))}
|
|
24187
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')||'')}}}
|
|
24188
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]}}}
|
|
24189
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}
|
|
@@ -24209,8 +26483,8 @@ function _chatVirtReleaseNode(node){
|
|
|
24209
26483
|
CHAT_VIRT.poolSize=Number(CHAT_VIRT.poolSize||0)+1;
|
|
24210
26484
|
}
|
|
24211
26485
|
function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
|
|
24212
|
-
function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager')?role:''}
|
|
24213
|
-
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''}
|
|
24214
26488
|
function _stripLeadingAgentTitle(raw,agentRole){
|
|
24215
26489
|
let txt=String(raw||'').replace(/^\\uFEFF/,'').trimStart();
|
|
24216
26490
|
const role=_chatVirtAgentRoleKey(agentRole);
|
|
@@ -24764,6 +27038,7 @@ function renderFileExplorer(){const host=E('fileExplorer');if(!host)return;const
|
|
|
24764
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}}
|
|
24765
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'}`;
|
|
24766
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)}
|
|
24767
27042
|
_renderBridgeSyncFromSnapshot(S.snap||{});
|
|
24768
27043
|
setPanelHtml('todos',renderTodoBoard(S.snap?.todos||[]));
|
|
24769
27044
|
setPanelHtml('tasks',renderTaskBoard(S.snap?.tasks||[]));
|
|
@@ -25034,9 +27309,10 @@ async function sendMessage(){showError('');const t=E('prompt').value.trim();if(!
|
|
|
25034
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})}}
|
|
25035
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})}}
|
|
25036
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))}}
|
|
25037
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})}
|
|
25038
27314
|
function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
|
|
25039
|
-
window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){const el=E(id);if(el){if(id==='chat'){continue}if(id==='sessionList'||id==='todos'||id==='tasks'){S.follow[id]=false;const mark=(lockMs=PANEL_SCROLL_ACTIVE_MS)=>{const now=Date.now();el._panelUserScrollTs=now;el._panelUserScrollLockTs=Math.max(Number(el._panelUserScrollLockTs||0),now+Math.max(PANEL_SCROLL_ACTIVE_MS,Number(lockMs)||PANEL_SCROLL_ACTIVE_MS))};el.addEventListener('wheel',()=>mark(PANEL_SCROLL_ACTIVE_MS+260),{passive:true});el.addEventListener('touchstart',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('touchmove',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('mousedown',()=>mark(PANEL_SCROLL_ACTIVE_MS+180),{passive:true});el.addEventListener('scroll',()=>mark(PANEL_SCROLL_ACTIVE_MS),{passive:true});continue}el.addEventListener('scroll',()=>{S.follow[id]=nearBottom(el)})}}const drop=E('uploadDrop');const fileInput=E('uploadInput');if(drop&&fileInput){drop.onclick=()=>fileInput.click();fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');uploadFiles(e.dataTransfer?.files||[]).catch(err=>showError(err.message))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('importConfigBtn',importDefaultConfig);bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('compactBtn',compactNow);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('refreshBtn',()=>refreshAll(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const exportMenuBtn=E('exportMenuBtn');const exportMenu=E('exportMenu');if(exportMenuBtn&&exportMenu){exportMenuBtn.addEventListener('click',e=>{e.stopPropagation();exportMenu.style.display=exportMenu.style.display==='none'?'block':'none'});document.addEventListener('click',()=>{exportMenu.style.display='none'});exportMenu.addEventListener('click',e=>{e.stopPropagation()});for(const a of exportMenu.querySelectorAll('.export-item')){a.addEventListener('click',()=>{exportMenu.style.display='none'})}}const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}const promptEl=E('prompt');if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length)await createSession()}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.deltaWatchdogTimer){clearTimeout(S.deltaWatchdogTimer);S.deltaWatchdogTimer=null}if(S.sessionPollTimer){clearTimeout(S.sessionPollTimer);S.sessionPollTimer=null}if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();_deltaStartWatchdog();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
|
|
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})})})
|
|
25040
27316
|
"""
|
|
25041
27317
|
|
|
25042
27318
|
APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
|
|
@@ -27357,6 +29633,20 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
27357
29633
|
return self._send_json({"error": "session not found"}, status=404)
|
|
27358
29634
|
sess.interrupt()
|
|
27359
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})
|
|
27360
29650
|
return self._send_json({"error": "not found"}, status=404)
|
|
27361
29651
|
|
|
27362
29652
|
def do_PATCH(self):
|