clouds-coder 2026.4.2.1__tar.gz → 2026.4.6__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.4.2.1 → clouds_coder-2026.4.6}/Clouds_Coder.py +1961 -338
- {clouds_coder-2026.4.2.1/clouds_coder.egg-info → clouds_coder-2026.4.6}/PKG-INFO +1 -1
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6/clouds_coder.egg-info}/PKG-INFO +1 -1
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/pyproject.toml +1 -1
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/LICENSE +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/README.md +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/setup.cfg +0 -0
- {clouds_coder-2026.4.2.1 → clouds_coder-2026.4.6}/tests/test_smoke.py +0 -0
|
@@ -192,7 +192,7 @@ REPEATED_TOOL_LOOP_THRESHOLD = 2
|
|
|
192
192
|
BASH_READ_LOOP_THRESHOLD = 3
|
|
193
193
|
HARD_BREAK_TOOL_ERROR_THRESHOLD = 20
|
|
194
194
|
HARD_BREAK_RECOVERY_ROUND_THRESHOLD = 3
|
|
195
|
-
FUSED_FAULT_BREAK_THRESHOLD =
|
|
195
|
+
FUSED_FAULT_BREAK_THRESHOLD = 15
|
|
196
196
|
STALL_SEVERITY_ESCALATION_THRESHOLD = 5
|
|
197
197
|
STALL_SEVERITY_WEIGHT_BASH_READ_LOOP = 2
|
|
198
198
|
STALL_SEVERITY_WEIGHT_REPEATED_TOOL = 3
|
|
@@ -216,6 +216,22 @@ DEFAULT_TIMEOUT_SECONDS = max(
|
|
|
216
216
|
),
|
|
217
217
|
)
|
|
218
218
|
DEFAULT_REQUEST_TIMEOUT = DEFAULT_TIMEOUT_SECONDS
|
|
219
|
+
# Patterns that indicate the shell process is waiting for interactive confirmation.
|
|
220
|
+
# Checked against the tail of combined stdout+stderr (lowercased bytes).
|
|
221
|
+
_SHELL_AUTO_CONFIRM_PATTERNS: tuple[bytes, ...] = (
|
|
222
|
+
b"ok to proceed? (y)",
|
|
223
|
+
b"proceed? (y)",
|
|
224
|
+
b"? (y)",
|
|
225
|
+
b"[y/n]",
|
|
226
|
+
b"[y/n]:",
|
|
227
|
+
b"[yes/no]",
|
|
228
|
+
b"(y/n)",
|
|
229
|
+
b"(yes/no)",
|
|
230
|
+
b"continue? (y/n)",
|
|
231
|
+
b"do you want to continue",
|
|
232
|
+
b"press enter to continue",
|
|
233
|
+
b"enter to continue",
|
|
234
|
+
)
|
|
219
235
|
MIN_SHELL_COMMAND_TIMEOUT_SECONDS = 10
|
|
220
236
|
MAX_SHELL_COMMAND_TIMEOUT_SECONDS = 86_400
|
|
221
237
|
DEFAULT_SHELL_COMMAND_TIMEOUT_SECONDS = max(
|
|
@@ -247,7 +263,7 @@ WATCHDOG_CONTEXT_NEAR_RATIO = 0.92
|
|
|
247
263
|
WATCHDOG_MAX_DECOMPOSE_STEPS = 12
|
|
248
264
|
WATCHDOG_STEP_MAX_ATTEMPTS = 2
|
|
249
265
|
EMPTY_ACTION_MIN_CONTENT_CHARS = 5
|
|
250
|
-
EMPTY_ACTION_WAKEUP_RETRY_LIMIT =
|
|
266
|
+
EMPTY_ACTION_WAKEUP_RETRY_LIMIT = 5
|
|
251
267
|
THINKING_BUDGET_FORCE_RATIO = 0.85
|
|
252
268
|
# --- Tool timeout configuration ---
|
|
253
269
|
_TOOL_TIMEOUT_MAP = {
|
|
@@ -398,7 +414,13 @@ BLACKBOARD_STATUSES = (
|
|
|
398
414
|
"COMPLETED",
|
|
399
415
|
"PAUSED",
|
|
400
416
|
)
|
|
401
|
-
TASK_COMPLEXITY_LEVELS = ("simple", "complex")
|
|
417
|
+
TASK_COMPLEXITY_LEVELS = ("simple", "moderate", "complex", "expert")
|
|
418
|
+
TASK_COMPLEXITY_RANKS = {
|
|
419
|
+
"simple": 1,
|
|
420
|
+
"moderate": 2,
|
|
421
|
+
"complex": 3,
|
|
422
|
+
"expert": 4,
|
|
423
|
+
}
|
|
402
424
|
TASK_PROFILE_TYPES = (
|
|
403
425
|
"simple_qa",
|
|
404
426
|
"simple_code",
|
|
@@ -435,7 +457,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
435
457
|
"assigned_expert": "developer",
|
|
436
458
|
"round_budget": 16,
|
|
437
459
|
"requires_user_confirmation": False,
|
|
438
|
-
"complexity": "
|
|
460
|
+
"complexity": "moderate",
|
|
439
461
|
},
|
|
440
462
|
4: {
|
|
441
463
|
"name": "complex_collaboration",
|
|
@@ -453,7 +475,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
|
|
|
453
475
|
"assigned_expert": "explorer",
|
|
454
476
|
"round_budget": 0, # 0 means unlimited by tier budget (still guarded by global safeguards).
|
|
455
477
|
"requires_user_confirmation": True,
|
|
456
|
-
"complexity": "
|
|
478
|
+
"complexity": "expert",
|
|
457
479
|
},
|
|
458
480
|
}
|
|
459
481
|
MANAGER_ROUTE_TARGETS = ("explorer", "developer", "reviewer", "finish")
|
|
@@ -520,7 +542,7 @@ TASK_PHASE_ROUTING = {
|
|
|
520
542
|
COMPLEXITY_KEYWORDS = (
|
|
521
543
|
"简单", "复杂", "难", "容易", "快速", "详细", "深入",
|
|
522
544
|
"l1", "l2", "l3", "l4", "l5",
|
|
523
|
-
"simple", "complex", "easy", "hard", "difficult",
|
|
545
|
+
"simple", "moderate", "medium", "complex", "expert", "easy", "hard", "difficult",
|
|
524
546
|
"thorough", "quick", "fast", "lightweight", "heavy",
|
|
525
547
|
)
|
|
526
548
|
USER_COMPLEXITY_SIMPLE_TOKENS = (
|
|
@@ -528,12 +550,23 @@ USER_COMPLEXITY_SIMPLE_TOKENS = (
|
|
|
528
550
|
"low", "simple", "easy", "quick", "fast", "lightweight", "basic", "minimal",
|
|
529
551
|
"l1", "l2",
|
|
530
552
|
)
|
|
553
|
+
USER_COMPLEXITY_MODERATE_TOKENS = (
|
|
554
|
+
"中等复杂度", "中等难度", "适中", "平衡", "标准", "普通", "常规",
|
|
555
|
+
"medium", "mid", "moderate", "balanced", "standard", "normal",
|
|
556
|
+
"l3",
|
|
557
|
+
)
|
|
531
558
|
USER_COMPLEXITY_COMPLEX_TOKENS = (
|
|
532
|
-
"复杂", "深入", "详细", "高复杂度", "高难度", "
|
|
533
|
-
"
|
|
534
|
-
"
|
|
559
|
+
"复杂", "深入", "详细", "高复杂度", "高难度", "中高复杂度",
|
|
560
|
+
"high", "complex", "hard", "difficult", "thorough", "detailed", "deep", "heavy",
|
|
561
|
+
"l4",
|
|
562
|
+
)
|
|
563
|
+
USER_COMPLEXITY_EXPERT_TOKENS = (
|
|
564
|
+
"专家级", "系統級", "系统级", "生产级", "企業級", "企业级", "高风险", "超高复杂度",
|
|
565
|
+
"expert", "advanced", "system-level", "production-ready", "enterprise", "mission-critical",
|
|
566
|
+
"l5",
|
|
535
567
|
)
|
|
536
568
|
PLAN_MODE_EXPLORER_MAX_ROUNDS = 8
|
|
569
|
+
PLAN_MODE_SYNTHESIS_MAX_ATTEMPTS = 3
|
|
537
570
|
# Reviewer debug mode
|
|
538
571
|
REVIEWER_DEBUG_MODE_MAX_ROUNDS = 6
|
|
539
572
|
REVIEWER_DEBUG_TOOL_ALLOWLIST = {
|
|
@@ -543,7 +576,7 @@ REVIEWER_DEBUG_TOOL_ALLOWLIST = {
|
|
|
543
576
|
}
|
|
544
577
|
EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before forced switch
|
|
545
578
|
DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same file before forced strategy change
|
|
546
|
-
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS =
|
|
579
|
+
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 8192
|
|
547
580
|
PLAN_MODE_MAX_OPTIONS = 3
|
|
548
581
|
PLAN_FILE_RELATIVE_PATH = ".clouds_coder/plan.md"
|
|
549
582
|
PLAN_BUBBLE_MAX_CHARS = 12_000
|
|
@@ -3086,19 +3119,243 @@ def decompress_text_blob(blob_b64: str) -> str:
|
|
|
3086
3119
|
except Exception:
|
|
3087
3120
|
return ""
|
|
3088
3121
|
|
|
3122
|
+
def normalize_embedded_newlines(text: object) -> str:
|
|
3123
|
+
s = str(text or "")
|
|
3124
|
+
if not s:
|
|
3125
|
+
return ""
|
|
3126
|
+
s = s.replace("\u2028", "\n").replace("\u2029", "\n")
|
|
3127
|
+
s = s.replace("\r\n", "\n").replace("\r", "\n")
|
|
3128
|
+
if "\\n" in s or "\\r" in s or "\\t" in s:
|
|
3129
|
+
s = s.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\r", "\n").replace("\\t", "\t")
|
|
3130
|
+
return s
|
|
3131
|
+
|
|
3132
|
+
|
|
3133
|
+
def _map_todo_status_token(token: str) -> str:
|
|
3134
|
+
raw = str(token or "").strip().lower().replace("_", " ").replace("-", " ")
|
|
3135
|
+
raw = re.sub(r"\s+", " ", raw)
|
|
3136
|
+
return {
|
|
3137
|
+
"pending": "pending",
|
|
3138
|
+
"待处理": "pending",
|
|
3139
|
+
"待處理": "pending",
|
|
3140
|
+
"未着手": "pending",
|
|
3141
|
+
"in progress": "in_progress",
|
|
3142
|
+
"进行中": "in_progress",
|
|
3143
|
+
"進行中": "in_progress",
|
|
3144
|
+
"completed": "completed",
|
|
3145
|
+
"已完成": "completed",
|
|
3146
|
+
"完了": "completed",
|
|
3147
|
+
"blocked": "pending",
|
|
3148
|
+
}.get(raw, "")
|
|
3149
|
+
|
|
3150
|
+
|
|
3151
|
+
def split_todo_status_text(text: object) -> tuple[str, str]:
|
|
3152
|
+
probe = normalize_embedded_newlines(text).strip()
|
|
3153
|
+
if not probe:
|
|
3154
|
+
return "", ""
|
|
3155
|
+
status = ""
|
|
3156
|
+
marker_prefix = r"(?:[-*•>]+\s*)?"
|
|
3157
|
+
for _ in range(4):
|
|
3158
|
+
before = probe
|
|
3159
|
+
probe = re.sub(r"^\s+", "", probe)
|
|
3160
|
+
matched = False
|
|
3161
|
+
for row_status, pattern in (
|
|
3162
|
+
(
|
|
3163
|
+
"completed",
|
|
3164
|
+
rf"^(?:{marker_prefix})(?:"
|
|
3165
|
+
rf"\[x\]\s*"
|
|
3166
|
+
rf")",
|
|
3167
|
+
),
|
|
3168
|
+
(
|
|
3169
|
+
"in_progress",
|
|
3170
|
+
rf"^(?:{marker_prefix})(?:"
|
|
3171
|
+
rf"\[>\]\s*"
|
|
3172
|
+
rf")",
|
|
3173
|
+
),
|
|
3174
|
+
(
|
|
3175
|
+
"pending",
|
|
3176
|
+
rf"^(?:{marker_prefix})(?:"
|
|
3177
|
+
rf"\[\s*\]\s*"
|
|
3178
|
+
rf")",
|
|
3179
|
+
),
|
|
3180
|
+
):
|
|
3181
|
+
m = re.match(pattern, probe, flags=re.IGNORECASE)
|
|
3182
|
+
if not m:
|
|
3183
|
+
continue
|
|
3184
|
+
status = row_status
|
|
3185
|
+
probe = probe[m.end():].strip()
|
|
3186
|
+
matched = True
|
|
3187
|
+
break
|
|
3188
|
+
if matched:
|
|
3189
|
+
continue
|
|
3190
|
+
m = re.match(
|
|
3191
|
+
rf"^(?:{marker_prefix})"
|
|
3192
|
+
rf"(pending|in[_\-\s]?progress|completed|blocked|"
|
|
3193
|
+
rf"待处理|待處理|未着手|进行中|進行中|已完成|完了)"
|
|
3194
|
+
rf"\s*[::\-\]]\s*",
|
|
3195
|
+
probe,
|
|
3196
|
+
flags=re.IGNORECASE,
|
|
3197
|
+
)
|
|
3198
|
+
if m:
|
|
3199
|
+
mapped = _map_todo_status_token(str(m.group(1) or ""))
|
|
3200
|
+
if mapped:
|
|
3201
|
+
status = mapped
|
|
3202
|
+
probe = probe[m.end():].strip()
|
|
3203
|
+
continue
|
|
3204
|
+
if probe == before:
|
|
3205
|
+
break
|
|
3206
|
+
return status, probe.strip()
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
def extract_todo_rows_from_text(
|
|
3210
|
+
text: object,
|
|
3211
|
+
*,
|
|
3212
|
+
default_parent_step_id: str = "",
|
|
3213
|
+
limit: int = 12,
|
|
3214
|
+
) -> list[dict]:
|
|
3215
|
+
src = normalize_embedded_newlines(text)
|
|
3216
|
+
if not src.strip():
|
|
3217
|
+
return []
|
|
3218
|
+
out: list[dict] = []
|
|
3219
|
+
seen: set[tuple[str, str, str]] = set()
|
|
3220
|
+
capped = max(1, min(40, int(limit or 12)))
|
|
3221
|
+
parent_step_id = trim(str(default_parent_step_id or "").strip(), 20)
|
|
3222
|
+
for raw_line in src.splitlines():
|
|
3223
|
+
line = trim(str(raw_line or "").strip(), 600)
|
|
3224
|
+
if not line:
|
|
3225
|
+
continue
|
|
3226
|
+
variants: list[str] = []
|
|
3227
|
+
for candidate in (
|
|
3228
|
+
line,
|
|
3229
|
+
re.sub(r"^\s*(?:[-*•>]+\s*)+", "", line).strip(),
|
|
3230
|
+
re.sub(r"^\s*\*\*([^*]+)\*\*\s*([::])\s*", r"\1\2 ", line).strip(),
|
|
3231
|
+
re.sub(r"^\s*(?:[-*•>]+\s*)*\*\*([^*]+)\*\*\s*([::])\s*", r"\1\2 ", line).strip(),
|
|
3232
|
+
):
|
|
3233
|
+
candidate = trim(str(candidate or "").strip(), 600)
|
|
3234
|
+
if candidate and candidate not in variants:
|
|
3235
|
+
variants.append(candidate)
|
|
3236
|
+
matched = False
|
|
3237
|
+
for candidate in variants:
|
|
3238
|
+
status, content = split_todo_status_text(candidate)
|
|
3239
|
+
if not status or not content:
|
|
3240
|
+
continue
|
|
3241
|
+
cleaned = normalize_work_text(content, status) or content
|
|
3242
|
+
cleaned = trim(cleaned.strip(), 400)
|
|
3243
|
+
if not cleaned:
|
|
3244
|
+
continue
|
|
3245
|
+
low = cleaned.lower()
|
|
3246
|
+
if low in {
|
|
3247
|
+
"todo",
|
|
3248
|
+
"todos",
|
|
3249
|
+
"task",
|
|
3250
|
+
"tasks",
|
|
3251
|
+
"subtask",
|
|
3252
|
+
"subtasks",
|
|
3253
|
+
"待办",
|
|
3254
|
+
"待辦",
|
|
3255
|
+
"子任务",
|
|
3256
|
+
"子任務",
|
|
3257
|
+
}:
|
|
3258
|
+
continue
|
|
3259
|
+
row = {"content": cleaned, "status": status}
|
|
3260
|
+
if parent_step_id:
|
|
3261
|
+
row["parent_step_id"] = parent_step_id
|
|
3262
|
+
identity = (
|
|
3263
|
+
status,
|
|
3264
|
+
normalize_work_text(cleaned, status).strip().lower(),
|
|
3265
|
+
parent_step_id,
|
|
3266
|
+
)
|
|
3267
|
+
if identity in seen:
|
|
3268
|
+
matched = True
|
|
3269
|
+
break
|
|
3270
|
+
seen.add(identity)
|
|
3271
|
+
out.append(row)
|
|
3272
|
+
matched = True
|
|
3273
|
+
break
|
|
3274
|
+
if matched and len(out) >= capped:
|
|
3275
|
+
break
|
|
3276
|
+
return out
|
|
3277
|
+
|
|
3278
|
+
|
|
3279
|
+
def infer_todo_status_from_text(text: object, default: str = "pending") -> str:
|
|
3280
|
+
status, content = split_todo_status_text(text)
|
|
3281
|
+
if not content and not status:
|
|
3282
|
+
return default
|
|
3283
|
+
if status:
|
|
3284
|
+
return status
|
|
3285
|
+
return default
|
|
3286
|
+
|
|
3287
|
+
|
|
3288
|
+
def split_structured_todo_content(text: object, limit: int = 7) -> list[str]:
|
|
3289
|
+
src = normalize_embedded_newlines(text).strip()
|
|
3290
|
+
if not src:
|
|
3291
|
+
return []
|
|
3292
|
+
lines = [trim(str(line or "").strip(), 500) for line in src.split("\n")]
|
|
3293
|
+
lines = [line for line in lines if line]
|
|
3294
|
+
if len(lines) <= 1:
|
|
3295
|
+
return [src]
|
|
3296
|
+
major_re = re.compile(r"^(\d+)\.\s+(.+)$")
|
|
3297
|
+
sub_re = re.compile(r"^(\d+)\.(\d+)\s+(.+)$")
|
|
3298
|
+
bullet_re = re.compile(r"^(?:[-*•]\s+)(.+)$")
|
|
3299
|
+
header_major = ""
|
|
3300
|
+
m0 = major_re.match(lines[0])
|
|
3301
|
+
if m0:
|
|
3302
|
+
header_major = str(m0.group(1) or "")
|
|
3303
|
+
picked: list[str] = []
|
|
3304
|
+
for idx, line in enumerate(lines):
|
|
3305
|
+
if idx == 0 and header_major:
|
|
3306
|
+
continue
|
|
3307
|
+
m_sub = sub_re.match(line)
|
|
3308
|
+
if m_sub:
|
|
3309
|
+
major = str(m_sub.group(1) or "")
|
|
3310
|
+
if header_major and major != header_major:
|
|
3311
|
+
if picked:
|
|
3312
|
+
break
|
|
3313
|
+
continue
|
|
3314
|
+
picked.append(f"{major}.{m_sub.group(2)} {trim(str(m_sub.group(3) or '').strip(), 420)}".strip())
|
|
3315
|
+
continue
|
|
3316
|
+
m_bullet = bullet_re.match(line)
|
|
3317
|
+
if m_bullet and (header_major or picked):
|
|
3318
|
+
picked.append(trim(str(m_bullet.group(1) or "").strip(), 420))
|
|
3319
|
+
continue
|
|
3320
|
+
if picked and re.match(r"^\d+\.\s+", line):
|
|
3321
|
+
break
|
|
3322
|
+
if not picked:
|
|
3323
|
+
for line in lines:
|
|
3324
|
+
m_sub = sub_re.match(line)
|
|
3325
|
+
if m_sub:
|
|
3326
|
+
picked.append(f"{m_sub.group(1)}.{m_sub.group(2)} {trim(str(m_sub.group(3) or '').strip(), 420)}".strip())
|
|
3327
|
+
if len(picked) >= max(1, int(limit or 7)):
|
|
3328
|
+
break
|
|
3329
|
+
if not picked:
|
|
3330
|
+
return [src]
|
|
3331
|
+
out: list[str] = []
|
|
3332
|
+
seen: set[str] = set()
|
|
3333
|
+
for line in picked:
|
|
3334
|
+
key = re.sub(r"\s+", " ", str(line or "").strip()).lower()
|
|
3335
|
+
if not key or key in seen:
|
|
3336
|
+
continue
|
|
3337
|
+
seen.add(key)
|
|
3338
|
+
out.append(line)
|
|
3339
|
+
if len(out) >= max(1, int(limit or 7)):
|
|
3340
|
+
break
|
|
3341
|
+
return out or [src]
|
|
3342
|
+
|
|
3343
|
+
|
|
3089
3344
|
def normalize_work_text(text: object, status: str = "") -> str:
|
|
3090
|
-
|
|
3345
|
+
parsed_status, parsed_content = split_todo_status_text(text)
|
|
3346
|
+
s = re.sub(r"\s+", " ", parsed_content or normalize_embedded_newlines(text)).strip()
|
|
3091
3347
|
if not s:
|
|
3092
3348
|
return ""
|
|
3093
|
-
s = re.sub(r"^\[[ x>\-]\]\s*", "", s, flags=re.IGNORECASE)
|
|
3094
3349
|
s = re.sub(
|
|
3095
|
-
r"^(pending|in[_\-\s]?progress|completed|done|blocked
|
|
3350
|
+
r"^(pending|todo|in[_\-\s]?progress|doing|working|completed|done|finished|blocked|"
|
|
3351
|
+
r"待处理|待處理|未着手|进行中|進行中|作業中|已完成|完成|完了)\s*[·::\-\]]\s*",
|
|
3096
3352
|
"",
|
|
3097
3353
|
s,
|
|
3098
3354
|
flags=re.IGNORECASE,
|
|
3099
3355
|
)
|
|
3100
|
-
|
|
3101
|
-
|
|
3356
|
+
status_key = _map_todo_status_token(status) or _map_todo_status_token(parsed_status) or str(status or "").strip().lower()
|
|
3357
|
+
if status_key:
|
|
3358
|
+
status_pattern = re.escape(status_key).replace("_", r"[_\-\s]?")
|
|
3102
3359
|
s = re.sub(
|
|
3103
3360
|
rf"\s*[—-]\s*{status_pattern}\s*$",
|
|
3104
3361
|
"",
|
|
@@ -3529,6 +3786,12 @@ def infer_user_complexity_value(text: str) -> str:
|
|
|
3529
3786
|
low = strip_thinking_content(str(text or "")).strip().lower()
|
|
3530
3787
|
if not low:
|
|
3531
3788
|
return ""
|
|
3789
|
+
for token in USER_COMPLEXITY_EXPERT_TOKENS:
|
|
3790
|
+
if re.search(rf"(?<![a-z0-9]){re.escape(token)}(?![a-z0-9])", low) if token.isascii() else token in low:
|
|
3791
|
+
return "expert"
|
|
3792
|
+
for token in USER_COMPLEXITY_MODERATE_TOKENS:
|
|
3793
|
+
if re.search(rf"(?<![a-z0-9]){re.escape(token)}(?![a-z0-9])", low) if token.isascii() else token in low:
|
|
3794
|
+
return "moderate"
|
|
3532
3795
|
for token in USER_COMPLEXITY_SIMPLE_TOKENS:
|
|
3533
3796
|
if re.search(rf"(?<![a-z0-9]){re.escape(token)}(?![a-z0-9])", low) if token.isascii() else token in low:
|
|
3534
3797
|
return "simple"
|
|
@@ -3537,6 +3800,53 @@ def infer_user_complexity_value(text: str) -> str:
|
|
|
3537
3800
|
return "complex"
|
|
3538
3801
|
return ""
|
|
3539
3802
|
|
|
3803
|
+
def normalize_task_complexity(raw: object, default: str = "simple") -> str:
|
|
3804
|
+
value = str(raw or "").strip().lower()
|
|
3805
|
+
aliases = {
|
|
3806
|
+
"simple": "simple",
|
|
3807
|
+
"low": "simple",
|
|
3808
|
+
"basic": "simple",
|
|
3809
|
+
"minimal": "simple",
|
|
3810
|
+
"moderate": "moderate",
|
|
3811
|
+
"medium": "moderate",
|
|
3812
|
+
"mid": "moderate",
|
|
3813
|
+
"balanced": "moderate",
|
|
3814
|
+
"standard": "moderate",
|
|
3815
|
+
"complex": "complex",
|
|
3816
|
+
"high": "complex",
|
|
3817
|
+
"hard": "complex",
|
|
3818
|
+
"difficult": "complex",
|
|
3819
|
+
"expert": "expert",
|
|
3820
|
+
"advanced": "expert",
|
|
3821
|
+
"system": "expert",
|
|
3822
|
+
"system_level": "expert",
|
|
3823
|
+
"production": "expert",
|
|
3824
|
+
}
|
|
3825
|
+
normalized = aliases.get(value, value)
|
|
3826
|
+
if normalized in TASK_COMPLEXITY_LEVELS:
|
|
3827
|
+
return normalized
|
|
3828
|
+
fallback = str(default or "").strip().lower()
|
|
3829
|
+
if not fallback:
|
|
3830
|
+
return ""
|
|
3831
|
+
return fallback if fallback in TASK_COMPLEXITY_LEVELS else "simple"
|
|
3832
|
+
|
|
3833
|
+
def task_complexity_rank(raw: object, default: str = "simple") -> int:
|
|
3834
|
+
return int(TASK_COMPLEXITY_RANKS.get(normalize_task_complexity(raw, default=default), 1))
|
|
3835
|
+
|
|
3836
|
+
def task_complexity_at_least(raw: object, threshold: str) -> bool:
|
|
3837
|
+
return task_complexity_rank(raw) >= task_complexity_rank(threshold)
|
|
3838
|
+
|
|
3839
|
+
def max_task_complexity(*values: object, default: str = "simple") -> str:
|
|
3840
|
+
best = normalize_task_complexity(default, default=default)
|
|
3841
|
+
best_rank = task_complexity_rank(best, default=default)
|
|
3842
|
+
for value in values:
|
|
3843
|
+
cur = normalize_task_complexity(value, default=default)
|
|
3844
|
+
cur_rank = task_complexity_rank(cur, default=default)
|
|
3845
|
+
if cur_rank > best_rank:
|
|
3846
|
+
best = cur
|
|
3847
|
+
best_rank = cur_rank
|
|
3848
|
+
return best
|
|
3849
|
+
|
|
3540
3850
|
def normalize_openai_compat_provider_name(raw: str) -> str:
|
|
3541
3851
|
value = str(raw or "").strip().lower().replace("-", "_")
|
|
3542
3852
|
aliases = {
|
|
@@ -5336,6 +5646,31 @@ class TodoManager:
|
|
|
5336
5646
|
def update(self, items: list[dict]) -> str:
|
|
5337
5647
|
if not isinstance(items, list):
|
|
5338
5648
|
raise ValueError("items must be array")
|
|
5649
|
+
expanded_items: list[dict] = []
|
|
5650
|
+
for item in items:
|
|
5651
|
+
if isinstance(item, str):
|
|
5652
|
+
raw = {"content": item, "status": "pending"}
|
|
5653
|
+
elif isinstance(item, dict):
|
|
5654
|
+
raw = dict(item)
|
|
5655
|
+
else:
|
|
5656
|
+
try:
|
|
5657
|
+
raw = {"content": str(item).strip(), "status": "pending"}
|
|
5658
|
+
except Exception:
|
|
5659
|
+
continue
|
|
5660
|
+
raw_content = str(raw.get("content", raw.get("text", raw.get("title", "")))).strip()
|
|
5661
|
+
split_rows = split_structured_todo_content(raw_content, limit=7)
|
|
5662
|
+
if len(split_rows) <= 1:
|
|
5663
|
+
expanded_items.append(raw)
|
|
5664
|
+
continue
|
|
5665
|
+
base_status = str(raw.get("status", raw.get("state", "pending")) or "pending").strip().lower()
|
|
5666
|
+
for split_idx, split_content in enumerate(split_rows):
|
|
5667
|
+
split_raw = dict(raw)
|
|
5668
|
+
split_raw["content"] = split_content
|
|
5669
|
+
split_raw["status"] = infer_todo_status_from_text(
|
|
5670
|
+
split_content,
|
|
5671
|
+
default=(base_status if split_idx == 0 else "pending"),
|
|
5672
|
+
)
|
|
5673
|
+
expanded_items.append(split_raw)
|
|
5339
5674
|
validated = []
|
|
5340
5675
|
# Plan-step items (bb:proj: key) keep a single in_progress slot.
|
|
5341
5676
|
# Worker/non-plan items allow one in_progress per owner so sync-mode agents
|
|
@@ -5351,18 +5686,10 @@ class TodoManager:
|
|
|
5351
5686
|
"finish": "completed",
|
|
5352
5687
|
"finished": "completed",
|
|
5353
5688
|
}
|
|
5354
|
-
for idx, item in enumerate(
|
|
5355
|
-
if isinstance(item, str):
|
|
5356
|
-
raw = {"content": item, "status": "pending"}
|
|
5357
|
-
elif isinstance(item, dict):
|
|
5358
|
-
raw = item
|
|
5359
|
-
else:
|
|
5360
|
-
# Tolerant: convert to string instead of raising
|
|
5361
|
-
try:
|
|
5362
|
-
raw = {"content": str(item).strip(), "status": "pending"}
|
|
5363
|
-
except Exception:
|
|
5364
|
-
continue # Skip unparseable items
|
|
5689
|
+
for idx, item in enumerate(expanded_items):
|
|
5690
|
+
raw = item if isinstance(item, dict) else {"content": str(item or "").strip(), "status": "pending"}
|
|
5365
5691
|
raw_content = str(raw.get("content", raw.get("text", raw.get("title", "")))).strip()
|
|
5692
|
+
inferred_status = infer_todo_status_from_text(raw_content, default="")
|
|
5366
5693
|
content = normalize_work_text(raw_content)
|
|
5367
5694
|
if not content:
|
|
5368
5695
|
content = raw_content
|
|
@@ -5370,8 +5697,10 @@ class TodoManager:
|
|
|
5370
5697
|
continue # Skip empty items instead of raising
|
|
5371
5698
|
raw_status = str(raw.get("status", raw.get("state", "pending"))).strip().lower()
|
|
5372
5699
|
status = status_alias.get(raw_status, raw_status or "pending")
|
|
5700
|
+
if inferred_status and status in {"", "pending", "todo"}:
|
|
5701
|
+
status = inferred_status
|
|
5373
5702
|
if status not in {"pending", "in_progress", "completed"}:
|
|
5374
|
-
status = "pending"
|
|
5703
|
+
status = inferred_status or "pending"
|
|
5375
5704
|
content = normalize_work_text(content, status) or content
|
|
5376
5705
|
active_form = str(
|
|
5377
5706
|
raw.get(
|
|
@@ -12526,10 +12855,10 @@ TOOLS = [
|
|
|
12526
12855
|
),
|
|
12527
12856
|
tool_def("write_file", "Write file content.", {"path": {"type": "string"}, "content": {"type": "string"}}, ["path", "content"]),
|
|
12528
12857
|
tool_def("edit_file", "Edit a file by replacing first match.", {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, ["path", "old_text", "new_text"]),
|
|
12529
|
-
tool_def("TodoWrite", "Update todo list.
|
|
12858
|
+
tool_def("TodoWrite", "Update todo list. Preferred format: objects with content/status/owner/parent_step_id. String fallback should use only '[ ] task', '[>] task', or '[x] task'.", {"items": {"type": "array", "items": {}}}, ["items"]),
|
|
12530
12859
|
tool_def(
|
|
12531
12860
|
"TodoWriteRescue",
|
|
12532
|
-
"Fallback todo writer.
|
|
12861
|
+
"Fallback todo writer. Preferred format: objects with content/status/owner/parent_step_id. String fallback should use only '[ ] task', '[>] task', or '[x] task'.",
|
|
12533
12862
|
{
|
|
12534
12863
|
"items": {"type": "array", "items": {}},
|
|
12535
12864
|
"in_progress_index": {"type": "integer"},
|
|
@@ -12963,6 +13292,8 @@ class SessionState:
|
|
|
12963
13292
|
self._cached_llm_complexity = ""
|
|
12964
13293
|
self._cached_complexity_dimensions: dict = {} # scope/steps/skill/output dimensions
|
|
12965
13294
|
self._pending_media_inputs: list[dict] = []
|
|
13295
|
+
self._pending_runtime_updates: list[dict] = []
|
|
13296
|
+
self._deferred_runtime_sync_requested = False
|
|
12966
13297
|
self.tool_retry_counts: dict[str, int] = {}
|
|
12967
13298
|
self.last_auto_title_ts = 0.0
|
|
12968
13299
|
self.live_thinking_text = ""
|
|
@@ -14459,9 +14790,9 @@ class SessionState:
|
|
|
14459
14790
|
)
|
|
14460
14791
|
if task_type in TASK_PROFILE_TYPES:
|
|
14461
14792
|
self.runtime_task_type = task_type
|
|
14462
|
-
complexity =
|
|
14463
|
-
|
|
14464
|
-
|
|
14793
|
+
complexity = normalize_task_complexity(
|
|
14794
|
+
profile.get("complexity", judgement.get("complexity", self.runtime_task_complexity or "")),
|
|
14795
|
+
default="simple",
|
|
14465
14796
|
)
|
|
14466
14797
|
if complexity in TASK_COMPLEXITY_LEVELS:
|
|
14467
14798
|
self.runtime_task_complexity = complexity
|
|
@@ -14931,12 +15262,15 @@ class SessionState:
|
|
|
14931
15262
|
|
|
14932
15263
|
def _current_plan_step_text(self, board: dict | None = None) -> str:
|
|
14933
15264
|
row = self._current_plan_step_row(board)
|
|
14934
|
-
|
|
15265
|
+
content = normalize_embedded_newlines((row or {}).get("content", "") or "").strip()
|
|
15266
|
+
if "\n" in content:
|
|
15267
|
+
content = content.split("\n", 1)[0].strip()
|
|
15268
|
+
return trim(content, 400)
|
|
14935
15269
|
|
|
14936
15270
|
def _current_plan_step_full_text(self, board: dict | None = None, max_len: int = 1200) -> str:
|
|
14937
15271
|
row = self._current_plan_step_row(board)
|
|
14938
15272
|
return trim(
|
|
14939
|
-
|
|
15273
|
+
normalize_embedded_newlines((row or {}).get("full_content", "") or (row or {}).get("content", "") or "").strip(),
|
|
14940
15274
|
max_len,
|
|
14941
15275
|
)
|
|
14942
15276
|
|
|
@@ -19985,11 +20319,17 @@ body{padding:18px}
|
|
|
19985
20319
|
with self.lock:
|
|
19986
20320
|
if self.running:
|
|
19987
20321
|
config_delayed = True
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
20322
|
+
if config_delayed:
|
|
20323
|
+
self._queue_deferred_runtime_update(
|
|
20324
|
+
"llm_config",
|
|
20325
|
+
{"config": cfg_obj, "source": workspace_rel},
|
|
20326
|
+
)
|
|
20327
|
+
loaded_config = self.model_catalog()
|
|
20328
|
+
if isinstance(loaded_config, dict):
|
|
20329
|
+
loaded_config["queued"] = True
|
|
20330
|
+
loaded_config["note"] = (
|
|
20331
|
+
"session is running; llm config queued and will apply after the current run finishes"
|
|
20332
|
+
)
|
|
19993
20333
|
if not config_delayed:
|
|
19994
20334
|
loaded_config = self.load_llm_config(cfg_obj, source=workspace_rel)
|
|
19995
20335
|
self._emit("config_applied", {
|
|
@@ -21155,7 +21495,7 @@ body{padding:18px}
|
|
|
21155
21495
|
return any(x in t for x in markers)
|
|
21156
21496
|
|
|
21157
21497
|
def _llm_classify_task_complexity(self, goal_text: str) -> str:
|
|
21158
|
-
"""LLM semantic pre-screening: classify task
|
|
21498
|
+
"""LLM semantic pre-screening: classify task into 4 complexity bands via 4-dimension analysis. 5s timeout."""
|
|
21159
21499
|
goal = trim(str(goal_text or ""), 400)
|
|
21160
21500
|
if not goal or len(goal) < 6:
|
|
21161
21501
|
return "simple"
|
|
@@ -21172,8 +21512,7 @@ body{padding:18px}
|
|
|
21172
21512
|
f"SKILL: does it need specialized tools, skills, research, or APIs?\n"
|
|
21173
21513
|
f"OUTPUT: what is expected (1=text answer, 2=single file, 3=system/multi-file)?\n\n"
|
|
21174
21514
|
f"Output exactly one line:\n"
|
|
21175
|
-
f"SCOPE:N STEPS:N SKILL:N OUTPUT:N VERDICT:SIMPLE|COMPLEX
|
|
21176
|
-
f"(COMPLEX if any dimension >= 2)"
|
|
21515
|
+
f"SCOPE:N STEPS:N SKILL:N OUTPUT:N VERDICT:SIMPLE|MODERATE|COMPLEX|EXPERT"
|
|
21177
21516
|
)}],
|
|
21178
21517
|
system="/no_think\nAnalyze task dimensions. One line output only.",
|
|
21179
21518
|
max_tokens=40,
|
|
@@ -21188,8 +21527,26 @@ body{padding:18px}
|
|
|
21188
21527
|
dims[dim.lower()] = int(m.group(1))
|
|
21189
21528
|
if dims:
|
|
21190
21529
|
self._cached_complexity_dimensions = dims
|
|
21191
|
-
|
|
21530
|
+
vals = [int(v) for v in dims.values()]
|
|
21531
|
+
max_dim = max(vals) if vals else 1
|
|
21532
|
+
count_ge2 = sum(1 for v in vals if int(v) >= 2)
|
|
21533
|
+
count_ge3 = sum(1 for v in vals if int(v) >= 3)
|
|
21534
|
+
if max_dim <= 1:
|
|
21535
|
+
result_box[0] = "simple"
|
|
21536
|
+
elif max_dim == 2:
|
|
21537
|
+
result_box[0] = "moderate"
|
|
21538
|
+
elif count_ge3 >= 2 or count_ge2 >= 4:
|
|
21539
|
+
result_box[0] = "expert"
|
|
21540
|
+
else:
|
|
21541
|
+
result_box[0] = "complex"
|
|
21542
|
+
if "VERDICT:EXPERT" in answer:
|
|
21543
|
+
result_box[0] = "expert"
|
|
21544
|
+
elif "VERDICT:COMPLEX" in answer:
|
|
21192
21545
|
result_box[0] = "complex"
|
|
21546
|
+
elif "VERDICT:MODERATE" in answer:
|
|
21547
|
+
result_box[0] = "moderate"
|
|
21548
|
+
elif "VERDICT:SIMPLE" in answer:
|
|
21549
|
+
result_box[0] = "simple"
|
|
21193
21550
|
except Exception:
|
|
21194
21551
|
pass
|
|
21195
21552
|
t = threading.Thread(target=_classify, daemon=True)
|
|
@@ -21202,9 +21559,9 @@ body{padding:18px}
|
|
|
21202
21559
|
low = clean.lower()
|
|
21203
21560
|
explicit_complexity = infer_user_complexity_value(clean)
|
|
21204
21561
|
# Use cached LLM complexity result (set by _agent_worker entry point)
|
|
21205
|
-
llm_complexity = str(getattr(self, '_cached_llm_complexity', '') or '')
|
|
21206
|
-
nontrivial = self._looks_nontrivial_request(clean) or llm_complexity
|
|
21207
|
-
direct_question = self._looks_like_direct_question_request(clean) and llm_complexity
|
|
21562
|
+
llm_complexity = normalize_task_complexity(str(getattr(self, '_cached_llm_complexity', '') or ''), default="simple")
|
|
21563
|
+
nontrivial = self._looks_nontrivial_request(clean) or task_complexity_at_least(llm_complexity, "moderate")
|
|
21564
|
+
direct_question = self._looks_like_direct_question_request(clean) and (not task_complexity_at_least(llm_complexity, "moderate"))
|
|
21208
21565
|
code_markers = [
|
|
21209
21566
|
# 代码/编程
|
|
21210
21567
|
"代码", "寫代碼", "写代码", "脚本", "模块", "函数", "class", "bug",
|
|
@@ -21241,6 +21598,7 @@ body{padding:18px}
|
|
|
21241
21598
|
has_code_intent = any(x in low for x in code_markers)
|
|
21242
21599
|
has_research_intent = any(x in low for x in research_markers)
|
|
21243
21600
|
length = len(clean)
|
|
21601
|
+
derived_complexity = max_task_complexity(explicit_complexity, llm_complexity, default="simple")
|
|
21244
21602
|
if direct_question and (not nontrivial) and (not has_code_intent) and length <= 220:
|
|
21245
21603
|
return {
|
|
21246
21604
|
"task_type": "simple_qa",
|
|
@@ -21271,7 +21629,11 @@ body{padding:18px}
|
|
|
21271
21629
|
if has_research_intent and (not has_code_intent):
|
|
21272
21630
|
return {
|
|
21273
21631
|
"task_type": "research",
|
|
21274
|
-
"complexity": explicit_complexity or (
|
|
21632
|
+
"complexity": explicit_complexity or max_task_complexity(
|
|
21633
|
+
derived_complexity,
|
|
21634
|
+
("complex" if length >= 480 else "moderate" if (nontrivial or length >= 280) else "simple"),
|
|
21635
|
+
default="simple",
|
|
21636
|
+
),
|
|
21275
21637
|
"direct_objective": "Collect evidence first, then synthesize a concise actionable answer.",
|
|
21276
21638
|
"recommended_agents": ["explorer", "developer", "reviewer"],
|
|
21277
21639
|
"round_budget": 10 if (nontrivial or length >= 280) else 6,
|
|
@@ -21282,7 +21644,15 @@ body{padding:18px}
|
|
|
21282
21644
|
if nontrivial or has_code_intent or length >= 280:
|
|
21283
21645
|
return {
|
|
21284
21646
|
"task_type": "engineering",
|
|
21285
|
-
"complexity": explicit_complexity or
|
|
21647
|
+
"complexity": explicit_complexity or max_task_complexity(
|
|
21648
|
+
derived_complexity,
|
|
21649
|
+
(
|
|
21650
|
+
"expert"
|
|
21651
|
+
if ((has_code_intent and has_research_intent) or length >= 900)
|
|
21652
|
+
else "complex"
|
|
21653
|
+
),
|
|
21654
|
+
default="moderate",
|
|
21655
|
+
),
|
|
21286
21656
|
"direct_objective": (
|
|
21287
21657
|
"Use blackboard collaboration to implement, validate, and converge with concrete outputs."
|
|
21288
21658
|
),
|
|
@@ -21294,7 +21664,7 @@ body{padding:18px}
|
|
|
21294
21664
|
}
|
|
21295
21665
|
return {
|
|
21296
21666
|
"task_type": "general",
|
|
21297
|
-
"complexity": explicit_complexity or "simple",
|
|
21667
|
+
"complexity": explicit_complexity or derived_complexity or "simple",
|
|
21298
21668
|
"direct_objective": (
|
|
21299
21669
|
"Provide the most direct useful response with minimal orchestration, "
|
|
21300
21670
|
"anchored to the current project context and user goal."
|
|
@@ -21556,6 +21926,66 @@ body{padding:18px}
|
|
|
21556
21926
|
model = str(profile.get("model", self.ollama.model) or self.ollama.model).strip()
|
|
21557
21927
|
return f"{self.active_profile_id}::{model}"
|
|
21558
21928
|
|
|
21929
|
+
def _queue_deferred_runtime_update(self, kind: str, payload: dict) -> int:
|
|
21930
|
+
row = {
|
|
21931
|
+
"kind": str(kind or "").strip().lower(),
|
|
21932
|
+
"payload": dict(payload or {}),
|
|
21933
|
+
"queued_at": float(now_ts()),
|
|
21934
|
+
}
|
|
21935
|
+
if not row["kind"]:
|
|
21936
|
+
raise ValueError("deferred runtime update kind required")
|
|
21937
|
+
with self.lock:
|
|
21938
|
+
self._pending_runtime_updates.append(row)
|
|
21939
|
+
self._pending_runtime_updates = self._pending_runtime_updates[-16:]
|
|
21940
|
+
queued = len(self._pending_runtime_updates)
|
|
21941
|
+
self.updated_at = now_ts()
|
|
21942
|
+
self._persist()
|
|
21943
|
+
return queued
|
|
21944
|
+
|
|
21945
|
+
def _apply_deferred_runtime_updates(self) -> list[str]:
|
|
21946
|
+
with self.lock:
|
|
21947
|
+
if self.running or not self._pending_runtime_updates:
|
|
21948
|
+
return []
|
|
21949
|
+
queued = list(self._pending_runtime_updates)
|
|
21950
|
+
self._pending_runtime_updates = []
|
|
21951
|
+
self.updated_at = now_ts()
|
|
21952
|
+
self._persist()
|
|
21953
|
+
applied_notes: list[str] = []
|
|
21954
|
+
sync_needed = False
|
|
21955
|
+
for item in queued:
|
|
21956
|
+
kind = str(item.get("kind", "") or "").strip().lower()
|
|
21957
|
+
payload = item.get("payload", {}) if isinstance(item.get("payload"), dict) else {}
|
|
21958
|
+
try:
|
|
21959
|
+
if kind == "llm_config":
|
|
21960
|
+
source = str(payload.get("source", "") or "deferred-config").strip()
|
|
21961
|
+
config = payload.get("config", {})
|
|
21962
|
+
if isinstance(config, dict) and config:
|
|
21963
|
+
self.load_llm_config(config, source=source)
|
|
21964
|
+
applied_notes.append(f"deferred llm config applied: {trim(source, 120)}")
|
|
21965
|
+
sync_needed = True
|
|
21966
|
+
elif kind == "model_selection":
|
|
21967
|
+
selection = str(payload.get("selection", "") or "").strip()
|
|
21968
|
+
model_override = payload.get("model_override")
|
|
21969
|
+
self.set_runtime_selection(
|
|
21970
|
+
selection,
|
|
21971
|
+
model_override if isinstance(model_override, str) else None,
|
|
21972
|
+
)
|
|
21973
|
+
applied_notes.append(f"deferred model switch applied: {trim(selection, 120)}")
|
|
21974
|
+
sync_needed = True
|
|
21975
|
+
except Exception as exc:
|
|
21976
|
+
self._emit(
|
|
21977
|
+
"status",
|
|
21978
|
+
{
|
|
21979
|
+
"summary": (
|
|
21980
|
+
f"deferred runtime update failed ({kind or 'unknown'}): "
|
|
21981
|
+
f"{trim(str(exc), 180)}"
|
|
21982
|
+
)
|
|
21983
|
+
},
|
|
21984
|
+
)
|
|
21985
|
+
if sync_needed:
|
|
21986
|
+
self._deferred_runtime_sync_requested = True
|
|
21987
|
+
return applied_notes
|
|
21988
|
+
|
|
21559
21989
|
def _global_wait_timeout_seconds(self) -> int:
|
|
21560
21990
|
raw = (
|
|
21561
21991
|
self.max_run_seconds
|
|
@@ -22107,6 +22537,7 @@ body{padding:18px}
|
|
|
22107
22537
|
_spawn_reader("stdout", proc.stdout)
|
|
22108
22538
|
_spawn_reader("stderr", proc.stderr)
|
|
22109
22539
|
|
|
22540
|
+
_auto_confirmed: set[bytes] = set()
|
|
22110
22541
|
while True:
|
|
22111
22542
|
now = time.time()
|
|
22112
22543
|
elapsed = now - start
|
|
@@ -22114,10 +22545,12 @@ body{padding:18px}
|
|
|
22114
22545
|
_stop_process(proc)
|
|
22115
22546
|
meta["error"] = "Error: interrupted by user"
|
|
22116
22547
|
meta["exit_code"] = -130
|
|
22548
|
+
break
|
|
22117
22549
|
elif (not meta.get("error")) and timeout > 0 and elapsed >= timeout:
|
|
22118
22550
|
_stop_process(proc)
|
|
22119
22551
|
meta["error"] = f"Error: timeout ({timeout}s)"
|
|
22120
22552
|
meta["exit_code"] = -1
|
|
22553
|
+
break
|
|
22121
22554
|
try:
|
|
22122
22555
|
label, chunk = io_queue.get(timeout=0.12)
|
|
22123
22556
|
if chunk is None:
|
|
@@ -22150,6 +22583,18 @@ body{padding:18px}
|
|
|
22150
22583
|
},
|
|
22151
22584
|
)
|
|
22152
22585
|
next_progress_emit = now + 0.8
|
|
22586
|
+
# Auto-confirm interactive prompts (Windows reader thread path)
|
|
22587
|
+
if proc.stdin and not proc.stdin.closed:
|
|
22588
|
+
_tail = (bytes(out_buf[-300:]) + bytes(err_buf[-300:])).lower()
|
|
22589
|
+
for _pat in _SHELL_AUTO_CONFIRM_PATTERNS:
|
|
22590
|
+
if _pat in _tail and _pat not in _auto_confirmed:
|
|
22591
|
+
try:
|
|
22592
|
+
proc.stdin.write(b"y\n")
|
|
22593
|
+
proc.stdin.flush()
|
|
22594
|
+
except Exception:
|
|
22595
|
+
pass
|
|
22596
|
+
_auto_confirmed.add(_pat)
|
|
22597
|
+
break
|
|
22153
22598
|
if (proc.poll() is not None) and (not active_readers) and io_queue.empty():
|
|
22154
22599
|
break
|
|
22155
22600
|
|
|
@@ -22190,6 +22635,7 @@ body{padding:18px}
|
|
|
22190
22635
|
popen_kwargs = {
|
|
22191
22636
|
"shell": True,
|
|
22192
22637
|
"cwd": cwd,
|
|
22638
|
+
"stdin": subprocess.PIPE,
|
|
22193
22639
|
"stdout": subprocess.PIPE,
|
|
22194
22640
|
"stderr": subprocess.PIPE,
|
|
22195
22641
|
"text": False,
|
|
@@ -22202,6 +22648,7 @@ body{padding:18px}
|
|
|
22202
22648
|
if create_group > 0:
|
|
22203
22649
|
popen_kwargs["creationflags"] = create_group
|
|
22204
22650
|
proc = subprocess.Popen(effective_command, **popen_kwargs)
|
|
22651
|
+
self._running_bash_proc = proc
|
|
22205
22652
|
if os.name == "nt":
|
|
22206
22653
|
# Windows: read PIPE output via blocking reader threads + queue.
|
|
22207
22654
|
_collect_with_reader_threads(proc)
|
|
@@ -22220,6 +22667,7 @@ body{padding:18px}
|
|
|
22220
22667
|
except Exception:
|
|
22221
22668
|
pass
|
|
22222
22669
|
sel.register(proc.stderr, selectors.EVENT_READ, data="stderr")
|
|
22670
|
+
_auto_confirmed: set[bytes] = set()
|
|
22223
22671
|
while True:
|
|
22224
22672
|
now = time.time()
|
|
22225
22673
|
elapsed = now - start
|
|
@@ -22227,10 +22675,12 @@ body{padding:18px}
|
|
|
22227
22675
|
_stop_process(proc)
|
|
22228
22676
|
meta["error"] = "Error: interrupted by user"
|
|
22229
22677
|
meta["exit_code"] = -130
|
|
22678
|
+
break
|
|
22230
22679
|
elif timeout > 0 and elapsed >= timeout:
|
|
22231
22680
|
_stop_process(proc)
|
|
22232
22681
|
meta["error"] = f"Error: timeout ({timeout}s)"
|
|
22233
22682
|
meta["exit_code"] = -1
|
|
22683
|
+
break
|
|
22234
22684
|
events = sel.select(timeout=0.12)
|
|
22235
22685
|
for key, _ in events:
|
|
22236
22686
|
stream = key.fileobj
|
|
@@ -22261,6 +22711,18 @@ body{padding:18px}
|
|
|
22261
22711
|
},
|
|
22262
22712
|
)
|
|
22263
22713
|
next_progress_emit = now + 0.8
|
|
22714
|
+
# Auto-confirm interactive prompts (e.g. "Ok to proceed? (y)")
|
|
22715
|
+
if proc.stdin and not proc.stdin.closed:
|
|
22716
|
+
_tail = (bytes(out_buf[-300:]) + bytes(err_buf[-300:])).lower()
|
|
22717
|
+
for _pat in _SHELL_AUTO_CONFIRM_PATTERNS:
|
|
22718
|
+
if _pat in _tail and _pat not in _auto_confirmed:
|
|
22719
|
+
try:
|
|
22720
|
+
proc.stdin.write(b"y\n")
|
|
22721
|
+
proc.stdin.flush()
|
|
22722
|
+
except Exception:
|
|
22723
|
+
pass
|
|
22724
|
+
_auto_confirmed.add(_pat)
|
|
22725
|
+
break
|
|
22264
22726
|
if (proc.poll() is not None) and (not sel.get_map()):
|
|
22265
22727
|
break
|
|
22266
22728
|
merged_raw = _merge_output_text()
|
|
@@ -22288,6 +22750,8 @@ body{padding:18px}
|
|
|
22288
22750
|
meta["error"] = f"Error: {exc}"
|
|
22289
22751
|
meta["output"] = meta["error"]
|
|
22290
22752
|
meta["exit_code"] = -1
|
|
22753
|
+
finally:
|
|
22754
|
+
self._running_bash_proc = None
|
|
22291
22755
|
meta["duration_ms"] = int((time.time() - start) * 1000)
|
|
22292
22756
|
after = self._git_status_map(cwd)
|
|
22293
22757
|
meta["changed_files"] = self._status_delta(before, after) if before or after else []
|
|
@@ -23175,9 +23639,10 @@ body{padding:18px}
|
|
|
23175
23639
|
) or str(base.get("task_type", "general"))
|
|
23176
23640
|
if task_type not in TASK_PROFILE_TYPES:
|
|
23177
23641
|
task_type = str(base.get("task_type", "general"))
|
|
23178
|
-
complexity =
|
|
23179
|
-
|
|
23180
|
-
|
|
23642
|
+
complexity = normalize_task_complexity(
|
|
23643
|
+
src.get("complexity", base.get("complexity", "simple")),
|
|
23644
|
+
default=str(base.get("complexity", "simple") or "simple"),
|
|
23645
|
+
)
|
|
23181
23646
|
src_direct_objective = trim(str(src.get("direct_objective", "") or "").strip(), 800)
|
|
23182
23647
|
legacy_objectives = {
|
|
23183
23648
|
"Provide the most direct useful response with minimal orchestration.",
|
|
@@ -23214,9 +23679,9 @@ body{padding:18px}
|
|
|
23214
23679
|
if raw_level not in TASK_LEVEL_CHOICES:
|
|
23215
23680
|
if task_type == "simple_qa":
|
|
23216
23681
|
raw_level = 1 if len(str(goal or "")) <= 180 else 2
|
|
23217
|
-
elif task_type in {"simple_code", "research"} and complexity
|
|
23682
|
+
elif task_type in {"simple_code", "research"} and task_complexity_rank(complexity) <= task_complexity_rank("moderate"):
|
|
23218
23683
|
raw_level = 3
|
|
23219
|
-
elif complexity
|
|
23684
|
+
elif task_complexity_at_least(complexity, "complex"):
|
|
23220
23685
|
raw_level = 4
|
|
23221
23686
|
else:
|
|
23222
23687
|
raw_level = 2
|
|
@@ -23305,7 +23770,7 @@ body{padding:18px}
|
|
|
23305
23770
|
goal = str(bb.get("original_goal", "") or "")
|
|
23306
23771
|
current = bb.get("task_profile", {})
|
|
23307
23772
|
profile = self._normalize_task_profile(goal, {} if force else current)
|
|
23308
|
-
if profile.get("complexity")
|
|
23773
|
+
if task_complexity_rank(profile.get("complexity", "simple")) < task_complexity_rank("complex"):
|
|
23309
23774
|
logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
|
|
23310
23775
|
tail = "\n".join(
|
|
23311
23776
|
str((row or {}).get("content", "") or "")
|
|
@@ -23409,10 +23874,16 @@ body{padding:18px}
|
|
|
23409
23874
|
# Project todo gate: coding tasks must pass compile + test
|
|
23410
23875
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
23411
23876
|
task_type = str(profile.get("task_type", "general") or "general")
|
|
23877
|
+
exec_mode = normalize_execution_mode(
|
|
23878
|
+
profile.get("execution_mode", self._effective_execution_mode()),
|
|
23879
|
+
default=self._effective_execution_mode(),
|
|
23880
|
+
)
|
|
23412
23881
|
if task_type in ("simple_code", "engineering"):
|
|
23413
23882
|
for todo in bb.get("project_todos", []):
|
|
23414
23883
|
if todo.get("category") in ("compile_test", "min_test") and todo.get("status") != "completed":
|
|
23415
23884
|
return False, f"project-todo-incomplete:{todo.get('category', '')}"
|
|
23885
|
+
if exec_mode == EXECUTION_MODE_SYNC and not self._manager_feedback_passed_from_blackboard(bb):
|
|
23886
|
+
return False, "sync-review-missing"
|
|
23416
23887
|
return True, "ok"
|
|
23417
23888
|
|
|
23418
23889
|
def _invalidate_stale_approval_if_needed(
|
|
@@ -23630,6 +24101,10 @@ body{padding:18px}
|
|
|
23630
24101
|
def _watchdog_state_fingerprint(self, board: dict | None = None) -> str:
|
|
23631
24102
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
23632
24103
|
profile = self._ensure_blackboard_task_profile(bb)
|
|
24104
|
+
step_snapshot = self._active_plan_progress_snapshot(bb)
|
|
24105
|
+
last_reply = bb.get("last_worker_reply", {}) if isinstance(bb.get("last_worker_reply"), dict) else {}
|
|
24106
|
+
last_reply_role = self._sanitize_agent_role(last_reply.get("role", ""))
|
|
24107
|
+
last_reply_text = trim(str(last_reply.get("text", "") or "").strip(), 240)
|
|
23633
24108
|
payload = {
|
|
23634
24109
|
"status": self._normalize_blackboard_status(bb.get("status", "INITIALIZING")),
|
|
23635
24110
|
"goal": trim(str(bb.get("original_goal", "") or "").strip(), 400),
|
|
@@ -23642,6 +24117,16 @@ body{padding:18px}
|
|
|
23642
24117
|
"approved": bool((bb.get("approval", {}) or {}).get("approved", False)),
|
|
23643
24118
|
"task_type": str(profile.get("task_type", "general") or "general"),
|
|
23644
24119
|
"complexity": str(profile.get("complexity", "simple") or "simple"),
|
|
24120
|
+
"plan_step_id": str(step_snapshot.get("step_id", "") or ""),
|
|
24121
|
+
"plan_step_text": trim(str(step_snapshot.get("step_text", "") or "").strip(), 180),
|
|
24122
|
+
"worker_todo_count": int(step_snapshot.get("worker_todo_count", 0) or 0),
|
|
24123
|
+
"worker_todo_completed": int(step_snapshot.get("completed_count", 0) or 0),
|
|
24124
|
+
"worker_todo_in_progress": int(step_snapshot.get("in_progress_count", 0) or 0),
|
|
24125
|
+
"worker_todo_pending": int(step_snapshot.get("pending_count", 0) or 0),
|
|
24126
|
+
"current_subtask": trim(str(step_snapshot.get("current_subtask", "") or "").strip(), 180),
|
|
24127
|
+
"next_pending_subtask": trim(str(step_snapshot.get("next_pending_subtask", "") or "").strip(), 180),
|
|
24128
|
+
"last_worker_reply_role": last_reply_role,
|
|
24129
|
+
"last_worker_reply_text": last_reply_text,
|
|
23645
24130
|
}
|
|
23646
24131
|
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
23647
24132
|
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
|
@@ -24286,6 +24771,7 @@ body{padding:18px}
|
|
|
24286
24771
|
"instruction": "",
|
|
24287
24772
|
"reason": "",
|
|
24288
24773
|
"source": "",
|
|
24774
|
+
"progress_fp": "",
|
|
24289
24775
|
"is_mandatory": False,
|
|
24290
24776
|
"ts": 0.0,
|
|
24291
24777
|
},
|
|
@@ -24341,6 +24827,7 @@ body{padding:18px}
|
|
|
24341
24827
|
"instruction": trim(str(raw_delegate.get("instruction", "") or "").strip(), 1200),
|
|
24342
24828
|
"reason": trim(str(raw_delegate.get("reason", "") or "").strip(), 600),
|
|
24343
24829
|
"source": trim(str(raw_delegate.get("source", "") or "").strip(), 40),
|
|
24830
|
+
"progress_fp": trim(str(raw_delegate.get("progress_fp", "") or "").strip(), 80),
|
|
24344
24831
|
"is_mandatory": _to_bool_like(raw_delegate.get("is_mandatory", False), default=False),
|
|
24345
24832
|
"ts": float(raw_delegate.get("ts", 0.0) or 0.0),
|
|
24346
24833
|
}
|
|
@@ -24478,8 +24965,8 @@ body{padding:18px}
|
|
|
24478
24965
|
for pt in bb_src_todos[:40]:
|
|
24479
24966
|
if not isinstance(pt, dict):
|
|
24480
24967
|
continue
|
|
24481
|
-
raw_content = trim(
|
|
24482
|
-
raw_full = trim(
|
|
24968
|
+
raw_content = trim(normalize_embedded_newlines(pt.get("content", "")), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
|
|
24969
|
+
raw_full = trim(normalize_embedded_newlines(pt.get("full_content", "")), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
|
|
24483
24970
|
# Migration: if full_content is empty but content has sub-steps, auto-split
|
|
24484
24971
|
if not raw_full and raw_content and pt.get("category") == "plan_step":
|
|
24485
24972
|
normalized = _mid_re_norm.sub(r"\n\1", raw_content)
|
|
@@ -24892,7 +25379,8 @@ body{padding:18px}
|
|
|
24892
25379
|
if not isinstance(fl, dict):
|
|
24893
25380
|
return
|
|
24894
25381
|
delegations = fl.get("repeated_delegations", [])
|
|
24895
|
-
|
|
25382
|
+
progress_fp = self._watchdog_state_fingerprint(bb)
|
|
25383
|
+
fp = hashlib.sha1((str(instruction or "") + "|" + progress_fp).encode("utf-8")).hexdigest()[:12]
|
|
24896
25384
|
for entry in delegations:
|
|
24897
25385
|
if entry.get("instruction_hash") == fp and entry.get("target") == target:
|
|
24898
25386
|
entry["count"] = int(entry.get("count", 1) or 1) + 1
|
|
@@ -24905,6 +25393,7 @@ body{padding:18px}
|
|
|
24905
25393
|
"target": trim(str(target or ""), 40),
|
|
24906
25394
|
"instruction_hash": fp,
|
|
24907
25395
|
"instruction_preview": trim(str(instruction or ""), 200),
|
|
25396
|
+
"progress_fp": progress_fp,
|
|
24908
25397
|
"count": 1,
|
|
24909
25398
|
"first_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
24910
25399
|
"last_round": int(getattr(self, "agent_round_index", 0) or 0),
|
|
@@ -24970,6 +25459,12 @@ body{padding:18px}
|
|
|
24970
25459
|
board["updated_at"] = float(now_ts())
|
|
24971
25460
|
self.blackboard = board
|
|
24972
25461
|
|
|
25462
|
+
def _save_blackboard(self, bb: dict):
|
|
25463
|
+
"""Persist a blackboard dict as the current blackboard and touch updated_at."""
|
|
25464
|
+
if isinstance(bb, dict):
|
|
25465
|
+
bb["updated_at"] = float(now_ts())
|
|
25466
|
+
self.blackboard = bb
|
|
25467
|
+
|
|
24973
25468
|
def _blackboard_reset_for_goal(self, goal: str):
|
|
24974
25469
|
# Preserve plan state when safe, but refresh loaded skills on goal change.
|
|
24975
25470
|
old_bb = self._ensure_blackboard()
|
|
@@ -25857,6 +26352,16 @@ body{padding:18px}
|
|
|
25857
26352
|
current["completed_at"] = float(now_ts())
|
|
25858
26353
|
current["completed_by"] = actor
|
|
25859
26354
|
current["evidence"] = trim(str(evidence or "").strip(), 200) or self._ui_text("step_completed_evidence")
|
|
26355
|
+
# Clear single-mode validation gate flags for the completed step
|
|
26356
|
+
try:
|
|
26357
|
+
_completed_id = str(current.get("id", "") or "")
|
|
26358
|
+
for _attr_name in (f"_smvg_{_completed_id}", f"_smvg_ts_{_completed_id}", f"_smvg_n_{_completed_id}", f"_sync_exec_gate_n_{_completed_id}", f"_sync_sv_ts_{_completed_id}"):
|
|
26359
|
+
try:
|
|
26360
|
+
delattr(self, _attr_name)
|
|
26361
|
+
except AttributeError:
|
|
26362
|
+
pass
|
|
26363
|
+
except Exception:
|
|
26364
|
+
pass
|
|
25860
26365
|
# 推进 cursor,激活下一步
|
|
25861
26366
|
cursor = int(bb.get("plan_step_cursor", 0) or 0)
|
|
25862
26367
|
bb["plan_step_cursor"] = cursor + 1
|
|
@@ -26024,7 +26529,33 @@ body{padding:18px}
|
|
|
26024
26529
|
)
|
|
26025
26530
|
) or accumulated_evidence_path
|
|
26026
26531
|
if has_strong_evidence:
|
|
26532
|
+
# Sync mode exec gate: when all subtasks done for implement/test/deploy phases,
|
|
26533
|
+
# require at least some execution evidence (bash/test/compile ran at any point).
|
|
26534
|
+
# Manager-requested advancement has its own escape hatch after 10 blocks.
|
|
26535
|
+
_exec_gate_needed = (
|
|
26536
|
+
subtasks_all_done
|
|
26537
|
+
and phase in ("implement", "test", "deploy")
|
|
26538
|
+
)
|
|
26539
|
+
if _exec_gate_needed:
|
|
26540
|
+
# Require model's explicit <step-verified/> tag in agent_messages since step activation
|
|
26541
|
+
_has_verified = self._check_step_verified_tag(current, messages=self.agent_messages)
|
|
26542
|
+
if not _has_verified:
|
|
26543
|
+
_sync_n_flag = f"_sync_exec_gate_n_{str(current.get('id', '') or '')}"
|
|
26544
|
+
_sync_n = int(getattr(self, _sync_n_flag, 0))
|
|
26545
|
+
if _sync_n < 10:
|
|
26546
|
+
setattr(self, _sync_n_flag, _sync_n + 1)
|
|
26547
|
+
# No verified tag yet — push worker to evaluate and emit <step-verified/>
|
|
26548
|
+
self._inject_sync_mode_verification_hint(current, worker_step)
|
|
26549
|
+
return
|
|
26550
|
+
# After 10 blocks, allow advancement to prevent permanent stall
|
|
26027
26551
|
evidence = self._collect_step_evidence(current, worker_step)
|
|
26552
|
+
# Clear sync exec gate counter on successful advance
|
|
26553
|
+
try:
|
|
26554
|
+
_sync_clear = f"_sync_exec_gate_n_{str(current.get('id', '') or '')}"
|
|
26555
|
+
if hasattr(self, _sync_clear):
|
|
26556
|
+
delattr(self, _sync_clear)
|
|
26557
|
+
except Exception:
|
|
26558
|
+
pass
|
|
26028
26559
|
self._advance_plan_step(
|
|
26029
26560
|
evidence=evidence,
|
|
26030
26561
|
actor=str(route.get("target", "developer") or "developer"),
|
|
@@ -26139,13 +26670,6 @@ body{padding:18px}
|
|
|
26139
26670
|
all_marked_done = all(str(r.get("status", "")).lower() == "completed" for r in worker_items)
|
|
26140
26671
|
if not all_marked_done:
|
|
26141
26672
|
return False
|
|
26142
|
-
# Acceptance verification: check that each "completed" subtask has real evidence
|
|
26143
|
-
# Don't just trust the model's TodoWrite status — verify against accumulated tool outputs
|
|
26144
|
-
if worker_items:
|
|
26145
|
-
bb = self._ensure_blackboard()
|
|
26146
|
-
unverified = self._verify_subtasks_acceptance(worker_items, step_id, bb)
|
|
26147
|
-
if unverified:
|
|
26148
|
-
return False
|
|
26149
26673
|
return True
|
|
26150
26674
|
|
|
26151
26675
|
def _verify_subtasks_acceptance(self, subtasks: list[dict], step_id: str, bb: dict) -> list[str]:
|
|
@@ -26392,6 +26916,10 @@ body{padding:18px}
|
|
|
26392
26916
|
parts.append(f"bash: {cmd}" + (f" => {out}" if out else ""))
|
|
26393
26917
|
elif name == "read_file":
|
|
26394
26918
|
path = str(r.get("args", {}).get("path", "") or "")
|
|
26919
|
+
# Skip plan-infrastructure reads — not meaningful implementation evidence
|
|
26920
|
+
_p = str(path)
|
|
26921
|
+
if (_p.endswith("plan.md") and ".clouds_coder" in _p) or ".clouds_coder/skills_cache/" in _p:
|
|
26922
|
+
continue
|
|
26395
26923
|
out = self._tool_result_output_excerpt(r, 90)
|
|
26396
26924
|
parts.append(f"read: {path}" + (f" => {out}" if out else ""))
|
|
26397
26925
|
elif name in ("write_to_blackboard", "query_code_library", "query_knowledge_library"):
|
|
@@ -26463,10 +26991,342 @@ body{padding:18px}
|
|
|
26463
26991
|
return False
|
|
26464
26992
|
return bool(self._active_plan_worker_todo_rows(step_id, role=role))
|
|
26465
26993
|
|
|
26994
|
+
def _bridge_flat_todos_to_active_plan_step(
|
|
26995
|
+
self,
|
|
26996
|
+
rows: list[dict] | None,
|
|
26997
|
+
board: dict | None = None,
|
|
26998
|
+
) -> tuple[list[dict], bool]:
|
|
26999
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27000
|
+
step = self._get_active_plan_step(bb)
|
|
27001
|
+
if not isinstance(step, dict):
|
|
27002
|
+
return (list(rows or []), False)
|
|
27003
|
+
step_id = trim(str(step.get("id", "") or ""), 20)
|
|
27004
|
+
if not step_id:
|
|
27005
|
+
return (list(rows or []), False)
|
|
27006
|
+
snap = [dict(row) for row in (rows or []) if isinstance(row, dict)]
|
|
27007
|
+
if not snap:
|
|
27008
|
+
return (snap, False)
|
|
27009
|
+
worker_owners = {"developer", "explorer", "reviewer"}
|
|
27010
|
+
if any(str(row.get("parent_step_id", "") or "").strip() for row in snap):
|
|
27011
|
+
return (snap, False)
|
|
27012
|
+
if any(
|
|
27013
|
+
str(row.get("owner", "") or "").strip().lower() in worker_owners
|
|
27014
|
+
and str(row.get("parent_step_id", "") or "").strip() == step_id
|
|
27015
|
+
for row in snap
|
|
27016
|
+
):
|
|
27017
|
+
return (snap, False)
|
|
27018
|
+
owner_key = self._current_plan_worker_owner(bb)
|
|
27019
|
+
bridged: list[dict] = []
|
|
27020
|
+
migrated = False
|
|
27021
|
+
for row in snap:
|
|
27022
|
+
key = trim(str(row.get("key", "") or "").strip(), 120)
|
|
27023
|
+
if key.startswith("bb:"):
|
|
27024
|
+
bridged.append(dict(row))
|
|
27025
|
+
continue
|
|
27026
|
+
content = normalize_work_text(str(row.get("content", "") or "")) or str(row.get("content", "") or "").strip()
|
|
27027
|
+
if not content:
|
|
27028
|
+
continue
|
|
27029
|
+
new_row = dict(row)
|
|
27030
|
+
new_row["content"] = content
|
|
27031
|
+
new_row["parent_step_id"] = step_id
|
|
27032
|
+
owner = str(new_row.get("owner", "") or "").strip().lower()
|
|
27033
|
+
if owner not in worker_owners:
|
|
27034
|
+
new_row["owner"] = owner_key
|
|
27035
|
+
bridged.append(new_row)
|
|
27036
|
+
migrated = True
|
|
27037
|
+
return (bridged, migrated)
|
|
27038
|
+
|
|
27039
|
+
def _active_plan_progress_snapshot(self, board: dict | None = None) -> dict:
|
|
27040
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27041
|
+
step = self._current_plan_step_row(bb)
|
|
27042
|
+
if not isinstance(step, dict):
|
|
27043
|
+
return {
|
|
27044
|
+
"step_id": "",
|
|
27045
|
+
"step_index": 0,
|
|
27046
|
+
"step_text": "",
|
|
27047
|
+
"expected_count": 0,
|
|
27048
|
+
"worker_todo_count": 0,
|
|
27049
|
+
"completed_count": 0,
|
|
27050
|
+
"in_progress_count": 0,
|
|
27051
|
+
"pending_count": 0,
|
|
27052
|
+
"current_subtask": "",
|
|
27053
|
+
"next_pending_subtask": "",
|
|
27054
|
+
"owners": [],
|
|
27055
|
+
}
|
|
27056
|
+
step_id = trim(str(step.get("id", "") or ""), 20)
|
|
27057
|
+
rows = self._active_plan_worker_todo_rows(step_id, role="") if step_id else []
|
|
27058
|
+
expected = self._extract_plan_step_subtasks(step, limit=5)
|
|
27059
|
+
completed_count = 0
|
|
27060
|
+
in_progress_count = 0
|
|
27061
|
+
pending_count = 0
|
|
27062
|
+
current_subtask = ""
|
|
27063
|
+
next_pending_subtask = ""
|
|
27064
|
+
owners: set[str] = set()
|
|
27065
|
+
for row in rows:
|
|
27066
|
+
status = str(row.get("status", "pending") or "pending").strip().lower()
|
|
27067
|
+
content = trim(str(row.get("content", "") or "").strip(), 220)
|
|
27068
|
+
owner = self._sanitize_agent_role(row.get("owner", ""))
|
|
27069
|
+
if owner:
|
|
27070
|
+
owners.add(owner)
|
|
27071
|
+
if status == "completed":
|
|
27072
|
+
completed_count += 1
|
|
27073
|
+
elif status == "in_progress":
|
|
27074
|
+
in_progress_count += 1
|
|
27075
|
+
if content and not current_subtask:
|
|
27076
|
+
current_subtask = content
|
|
27077
|
+
else:
|
|
27078
|
+
pending_count += 1
|
|
27079
|
+
if content and not next_pending_subtask:
|
|
27080
|
+
next_pending_subtask = content
|
|
27081
|
+
return {
|
|
27082
|
+
"step_id": step_id,
|
|
27083
|
+
"step_index": max(0, int(step.get("plan_step_index", 0) or 0)),
|
|
27084
|
+
"step_text": self._current_plan_step_text(bb),
|
|
27085
|
+
"expected_count": len(expected),
|
|
27086
|
+
"worker_todo_count": len(rows),
|
|
27087
|
+
"completed_count": completed_count,
|
|
27088
|
+
"in_progress_count": in_progress_count,
|
|
27089
|
+
"pending_count": pending_count,
|
|
27090
|
+
"current_subtask": current_subtask,
|
|
27091
|
+
"next_pending_subtask": next_pending_subtask,
|
|
27092
|
+
"owners": sorted(owners),
|
|
27093
|
+
}
|
|
27094
|
+
|
|
27095
|
+
def _manager_worker_progress_capsule(self, role: str, step: dict, board: dict | None = None) -> str:
|
|
27096
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27097
|
+
role_key = self._sanitize_agent_role(role) or "developer"
|
|
27098
|
+
safe_step = step if isinstance(step, dict) else {}
|
|
27099
|
+
snapshot = self._active_plan_progress_snapshot(bb)
|
|
27100
|
+
tool_results = safe_step.get("tool_results", []) or []
|
|
27101
|
+
tool_names: list[str] = []
|
|
27102
|
+
for item in tool_results:
|
|
27103
|
+
if not isinstance(item, dict):
|
|
27104
|
+
continue
|
|
27105
|
+
name = str(item.get("name", "") or "").strip()
|
|
27106
|
+
if not name or name in tool_names:
|
|
27107
|
+
continue
|
|
27108
|
+
tool_names.append(name)
|
|
27109
|
+
if len(tool_names) >= 5:
|
|
27110
|
+
break
|
|
27111
|
+
parts = [
|
|
27112
|
+
f"[worker-progress] owner={role_key}",
|
|
27113
|
+
f"status={trim(str(safe_step.get('status', '') or ''), 40) or '-'}",
|
|
27114
|
+
]
|
|
27115
|
+
if tool_names:
|
|
27116
|
+
parts.append("tools=" + ",".join(tool_names))
|
|
27117
|
+
step_text = trim(str(snapshot.get("step_text", "") or ""), 180)
|
|
27118
|
+
if step_text:
|
|
27119
|
+
parts.append(f"step={step_text}")
|
|
27120
|
+
todo_state = (
|
|
27121
|
+
f"todos={int(snapshot.get('completed_count', 0) or 0)}/"
|
|
27122
|
+
f"{int(snapshot.get('in_progress_count', 0) or 0)}/"
|
|
27123
|
+
f"{int(snapshot.get('pending_count', 0) or 0)}"
|
|
27124
|
+
)
|
|
27125
|
+
if int(snapshot.get("worker_todo_count", 0) or 0) > 0:
|
|
27126
|
+
parts.append(todo_state)
|
|
27127
|
+
elif int(snapshot.get("expected_count", 0) or 0) > 0:
|
|
27128
|
+
parts.append(f"todos=missing/{int(snapshot.get('expected_count', 0) or 0)}")
|
|
27129
|
+
focus = trim(str(snapshot.get("current_subtask", "") or ""), 160)
|
|
27130
|
+
if focus:
|
|
27131
|
+
parts.append(f"focus={focus}")
|
|
27132
|
+
elif str(snapshot.get("next_pending_subtask", "") or "").strip():
|
|
27133
|
+
parts.append(f"next={trim(str(snapshot.get('next_pending_subtask', '') or ''), 160)}")
|
|
27134
|
+
current_step = self._current_plan_step_row(bb)
|
|
27135
|
+
if isinstance(current_step, dict):
|
|
27136
|
+
evidence = self._collect_blackboard_step_evidence(current_step, bb)
|
|
27137
|
+
if evidence:
|
|
27138
|
+
parts.append(f"evidence={trim(evidence, 180)}")
|
|
27139
|
+
if self._step_subtasks_all_completed(current_step) and self._plan_step_has_blackboard_evidence(current_step, bb):
|
|
27140
|
+
parts.append("acceptance=ready")
|
|
27141
|
+
reply = bb.get("last_worker_reply", {}) if isinstance(bb.get("last_worker_reply"), dict) else {}
|
|
27142
|
+
if self._sanitize_agent_role(reply.get("role", "")) == role_key:
|
|
27143
|
+
reply_text = trim(str(reply.get("text", "") or "").strip(), 180)
|
|
27144
|
+
if reply_text:
|
|
27145
|
+
parts.append(f"reply={reply_text}")
|
|
27146
|
+
return trim(" | ".join(parts), 1600)
|
|
27147
|
+
|
|
27148
|
+
def _manager_recovery_route_for_repeated_delegate(self, route: dict, board: dict | None = None) -> dict:
|
|
27149
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27150
|
+
row = dict(route or {})
|
|
27151
|
+
step = self._current_plan_step_row(bb)
|
|
27152
|
+
if not isinstance(step, dict):
|
|
27153
|
+
row["target"] = "developer"
|
|
27154
|
+
row["instruction"] = (
|
|
27155
|
+
"Recovery routing after repeated identical delegation. "
|
|
27156
|
+
"Continue the current objective with one concrete tool action and write observable progress."
|
|
27157
|
+
)
|
|
27158
|
+
row["reason"] = trim(f"{row.get('reason', '')}|loop-recovery-no-plan-step", 600)
|
|
27159
|
+
row["source"] = "loop-recovery"
|
|
27160
|
+
row["is_mandatory"] = True
|
|
27161
|
+
return row
|
|
27162
|
+
snapshot = self._active_plan_progress_snapshot(bb)
|
|
27163
|
+
step_text = trim(str(snapshot.get("step_text", "") or ""), 220)
|
|
27164
|
+
full_text = self._current_plan_step_full_text(bb, max_len=600)
|
|
27165
|
+
phase = self._plan_step_phase_hint(full_text)
|
|
27166
|
+
expected_count = int(snapshot.get("expected_count", 0) or 0)
|
|
27167
|
+
worker_todo_count = int(snapshot.get("worker_todo_count", 0) or 0)
|
|
27168
|
+
current_subtask = trim(str(snapshot.get("current_subtask", "") or ""), 180)
|
|
27169
|
+
next_pending = trim(str(snapshot.get("next_pending_subtask", "") or ""), 180)
|
|
27170
|
+
subtasks_done = self._step_subtasks_all_completed(step)
|
|
27171
|
+
has_evidence = self._plan_step_has_blackboard_evidence(step, bb)
|
|
27172
|
+
reviewer_available = True
|
|
27173
|
+
profile = self._ensure_blackboard_task_profile(bb)
|
|
27174
|
+
participants = profile.get("participants", []) if isinstance(profile.get("participants"), list) else []
|
|
27175
|
+
participants_norm = [self._sanitize_agent_role(x) for x in participants]
|
|
27176
|
+
participants_norm = [x for x in participants_norm if x]
|
|
27177
|
+
if participants_norm:
|
|
27178
|
+
reviewer_available = "reviewer" in participants_norm
|
|
27179
|
+
if subtasks_done and has_evidence and reviewer_available:
|
|
27180
|
+
row["target"] = "reviewer"
|
|
27181
|
+
row["instruction"] = trim(
|
|
27182
|
+
(
|
|
27183
|
+
"Recovery routing after repeated identical delegation. "
|
|
27184
|
+
f"Validate ONLY the current plan step: {step_text}. "
|
|
27185
|
+
"Worker subtasks are complete and blackboard evidence already exists. "
|
|
27186
|
+
"Run acceptance for this step only, record pass/fix with concrete evidence, and do not jump ahead."
|
|
27187
|
+
),
|
|
27188
|
+
1200,
|
|
27189
|
+
)
|
|
27190
|
+
row["reason"] = trim(f"{row.get('reason', '')}|loop-recovery-acceptance", 600)
|
|
27191
|
+
row["source"] = "loop-recovery"
|
|
27192
|
+
row["is_mandatory"] = True
|
|
27193
|
+
return row
|
|
27194
|
+
default_owner = "explorer" if phase in {"research", "design"} else "developer"
|
|
27195
|
+
owner = self._sanitize_agent_role(row.get("target", "")) or self._current_plan_worker_owner(bb)
|
|
27196
|
+
if owner not in {"developer", "explorer"}:
|
|
27197
|
+
owner = default_owner
|
|
27198
|
+
if expected_count > 0 and worker_todo_count == 0:
|
|
27199
|
+
action_text = (
|
|
27200
|
+
"First call TodoWrite for the current plan step and create the missing worker subtasks "
|
|
27201
|
+
"before any more implementation."
|
|
27202
|
+
)
|
|
27203
|
+
elif current_subtask:
|
|
27204
|
+
action_text = f"Continue ONLY the current in_progress subtask: {current_subtask}."
|
|
27205
|
+
elif next_pending:
|
|
27206
|
+
action_text = f"Resume the next pending subtask: {next_pending}."
|
|
27207
|
+
else:
|
|
27208
|
+
action_text = "Continue the current plan step with one concrete tool action."
|
|
27209
|
+
evidence_text = ""
|
|
27210
|
+
if has_evidence:
|
|
27211
|
+
evidence_text = (
|
|
27212
|
+
" Blackboard already contains partial evidence for this step; build on that work instead of restarting."
|
|
27213
|
+
)
|
|
27214
|
+
row["target"] = owner
|
|
27215
|
+
row["instruction"] = trim(
|
|
27216
|
+
(
|
|
27217
|
+
"Recovery routing after repeated identical delegation. "
|
|
27218
|
+
f"Stay on the current plan step: {step_text}. "
|
|
27219
|
+
f"{action_text} "
|
|
27220
|
+
"Do not branch to a different step or restate the whole plan. "
|
|
27221
|
+
"After the subtask is finished, immediately call TodoWrite to mark it completed and move the next subtask to in_progress."
|
|
27222
|
+
f"{evidence_text}"
|
|
27223
|
+
),
|
|
27224
|
+
1200,
|
|
27225
|
+
)
|
|
27226
|
+
row["reason"] = trim(f"{row.get('reason', '')}|loop-recovery-execute", 600)
|
|
27227
|
+
row["source"] = "loop-recovery"
|
|
27228
|
+
row["is_mandatory"] = True
|
|
27229
|
+
return row
|
|
27230
|
+
|
|
27231
|
+
def _todo_has_plan_steps(self, board: dict | None = None) -> bool:
|
|
27232
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27233
|
+
todos = bb.get("project_todos", []) if isinstance(bb.get("project_todos"), list) else []
|
|
27234
|
+
return any(
|
|
27235
|
+
isinstance(todo, dict) and todo.get("category") == "plan_step"
|
|
27236
|
+
for todo in todos
|
|
27237
|
+
)
|
|
27238
|
+
|
|
27239
|
+
def _todo_worker_role_hint(self, role: str = "", board: dict | None = None) -> str:
|
|
27240
|
+
role_key = self._sanitize_agent_role(role)
|
|
27241
|
+
if role_key in {"developer", "explorer", "reviewer"}:
|
|
27242
|
+
return role_key
|
|
27243
|
+
return self._current_plan_worker_owner(board)
|
|
27244
|
+
|
|
27245
|
+
def _todo_route_kind(self, role: str = "", board: dict | None = None) -> str:
|
|
27246
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27247
|
+
if self._todo_has_plan_steps(bb):
|
|
27248
|
+
return "plan_sync" if self._is_multi_agent_mode() else "plan_single"
|
|
27249
|
+
role_key = self._todo_worker_role_hint(role, bb)
|
|
27250
|
+
if self._is_multi_agent_mode() and role_key in {"developer", "explorer", "reviewer"}:
|
|
27251
|
+
return "pure_sync"
|
|
27252
|
+
return "pure_single"
|
|
27253
|
+
|
|
27254
|
+
def _todo_row_kind(self, row: dict | None) -> str:
|
|
27255
|
+
if not isinstance(row, dict):
|
|
27256
|
+
return ""
|
|
27257
|
+
key = str(row.get("key", "") or "").strip()
|
|
27258
|
+
if key.startswith("bb:"):
|
|
27259
|
+
return "system"
|
|
27260
|
+
owner = str(row.get("owner", "") or "").strip().lower()
|
|
27261
|
+
parent_step_id = str(row.get("parent_step_id", "") or "").strip()
|
|
27262
|
+
if owner in {"developer", "explorer", "reviewer"} and parent_step_id:
|
|
27263
|
+
return "plan_worker"
|
|
27264
|
+
if owner in {"developer", "explorer", "reviewer"}:
|
|
27265
|
+
return "owner_worker"
|
|
27266
|
+
return "flat"
|
|
27267
|
+
|
|
27268
|
+
def _todo_route_rows(
|
|
27269
|
+
self,
|
|
27270
|
+
route_kind: str,
|
|
27271
|
+
*,
|
|
27272
|
+
rows: list[dict] | None = None,
|
|
27273
|
+
role: str = "",
|
|
27274
|
+
board: dict | None = None,
|
|
27275
|
+
) -> list[dict]:
|
|
27276
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27277
|
+
snap = [dict(row) for row in (rows if isinstance(rows, list) else self.todo.snapshot()) if isinstance(row, dict)]
|
|
27278
|
+
if route_kind in {"plan_single", "plan_sync"}:
|
|
27279
|
+
step = self._get_active_plan_step(bb)
|
|
27280
|
+
step_id = trim(str((step or {}).get("id", "") or ""), 20)
|
|
27281
|
+
if not step_id:
|
|
27282
|
+
return []
|
|
27283
|
+
return [
|
|
27284
|
+
row for row in snap
|
|
27285
|
+
if self._todo_row_kind(row) == "plan_worker"
|
|
27286
|
+
and str(row.get("parent_step_id", "") or "").strip() == step_id
|
|
27287
|
+
]
|
|
27288
|
+
if route_kind == "pure_sync":
|
|
27289
|
+
role_key = self._todo_worker_role_hint(role, bb)
|
|
27290
|
+
owner_rows = [row for row in snap if self._todo_row_kind(row) == "owner_worker"]
|
|
27291
|
+
if role_key in {"developer", "explorer", "reviewer"}:
|
|
27292
|
+
scoped = [
|
|
27293
|
+
row for row in owner_rows
|
|
27294
|
+
if str(row.get("owner", "") or "").strip().lower() == role_key
|
|
27295
|
+
]
|
|
27296
|
+
if scoped:
|
|
27297
|
+
return scoped
|
|
27298
|
+
return owner_rows
|
|
27299
|
+
if route_kind == "pure_single":
|
|
27300
|
+
return [row for row in snap if self._todo_row_kind(row) == "flat"]
|
|
27301
|
+
return []
|
|
27302
|
+
|
|
26466
27303
|
def _todo_runtime_has_worker_rows(self, role: str = "") -> bool:
|
|
26467
|
-
|
|
26468
|
-
|
|
26469
|
-
|
|
27304
|
+
route_kind = self._todo_route_kind(role=role)
|
|
27305
|
+
return bool(self._todo_route_rows(route_kind, role=role))
|
|
27306
|
+
|
|
27307
|
+
def _merge_todo_signal_rows(self, items: list[dict], role: str = "", board: dict | None = None) -> str:
|
|
27308
|
+
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27309
|
+
role_key = self._sanitize_agent_role(role)
|
|
27310
|
+
route_kind = self._todo_route_kind(role=role_key, board=bb)
|
|
27311
|
+
step = self._get_active_plan_step(bb) if route_kind in {"plan_single", "plan_sync"} else None
|
|
27312
|
+
step_id = trim(str((step or {}).get("id", "") or ""), 20)
|
|
27313
|
+
normalized: list[dict] = []
|
|
27314
|
+
for item in items or []:
|
|
27315
|
+
if not isinstance(item, dict):
|
|
27316
|
+
continue
|
|
27317
|
+
row = dict(item)
|
|
27318
|
+
if role_key in {"developer", "explorer", "reviewer"} and not str(row.get("owner", "") or "").strip():
|
|
27319
|
+
row["owner"] = role_key
|
|
27320
|
+
if step_id and not str(row.get("parent_step_id", "") or "").strip():
|
|
27321
|
+
row["parent_step_id"] = step_id
|
|
27322
|
+
normalized.append(row)
|
|
27323
|
+
if not normalized:
|
|
27324
|
+
return self.todo.no_changes_text()
|
|
27325
|
+
if route_kind in {"plan_single", "plan_sync"}:
|
|
27326
|
+
return self._merge_plan_worker_todo_items(normalized, role=role_key)
|
|
27327
|
+
if route_kind == "pure_sync":
|
|
27328
|
+
return self._merge_owner_scoped_todo_items(normalized, role=role_key)
|
|
27329
|
+
return self._merge_flat_todo_items(normalized, role=role_key)
|
|
26470
27330
|
|
|
26471
27331
|
def _plan_worker_todo_identity(self, row: dict | None) -> str:
|
|
26472
27332
|
import re
|
|
@@ -26482,6 +27342,96 @@ body{padding:18px}
|
|
|
26482
27342
|
return f"substep:{match.group(1)}"
|
|
26483
27343
|
return f"text:{content}"
|
|
26484
27344
|
|
|
27345
|
+
def _flat_todo_identity(self, row: dict | None) -> str:
|
|
27346
|
+
import re
|
|
27347
|
+
|
|
27348
|
+
if not isinstance(row, dict):
|
|
27349
|
+
return ""
|
|
27350
|
+
key = trim(str(row.get("key", "") or "").strip(), 120)
|
|
27351
|
+
if key.startswith("bb:"):
|
|
27352
|
+
return f"system:{key}"
|
|
27353
|
+
content = normalize_work_text(str(row.get("content", "") or "")) or str(row.get("content", "") or "")
|
|
27354
|
+
content = re.sub(r"\s+", " ", content.strip().lower())
|
|
27355
|
+
if not content:
|
|
27356
|
+
return ""
|
|
27357
|
+
match = re.match(r"^(\d+\.\d+)\b", content)
|
|
27358
|
+
if match:
|
|
27359
|
+
return f"substep:{match.group(1)}"
|
|
27360
|
+
return f"text:{content}"
|
|
27361
|
+
|
|
27362
|
+
def _merge_flat_todo_items(self, items: list[dict], role: str = "") -> str:
|
|
27363
|
+
if not isinstance(items, list):
|
|
27364
|
+
raise ValueError("items must be array")
|
|
27365
|
+
role_key = self._sanitize_agent_role(role)
|
|
27366
|
+
existing = self.todo.snapshot()
|
|
27367
|
+
route_existing = self._todo_route_rows("pure_single", rows=existing, role=role_key)
|
|
27368
|
+
existing_by_identity: dict[str, dict] = {}
|
|
27369
|
+
preserved_system: list[dict] = []
|
|
27370
|
+
for row in existing:
|
|
27371
|
+
if self._todo_row_kind(row) != "system":
|
|
27372
|
+
continue
|
|
27373
|
+
preserved_system.append(dict(row))
|
|
27374
|
+
for row in route_existing:
|
|
27375
|
+
if not isinstance(row, dict):
|
|
27376
|
+
continue
|
|
27377
|
+
identity = self._flat_todo_identity(row)
|
|
27378
|
+
if not identity:
|
|
27379
|
+
continue
|
|
27380
|
+
if identity not in existing_by_identity:
|
|
27381
|
+
existing_by_identity[identity] = dict(row)
|
|
27382
|
+
|
|
27383
|
+
status_alias = {
|
|
27384
|
+
"todo": "pending",
|
|
27385
|
+
"doing": "in_progress",
|
|
27386
|
+
"inprogress": "in_progress",
|
|
27387
|
+
"in-progress": "in_progress",
|
|
27388
|
+
"done": "completed",
|
|
27389
|
+
"finish": "completed",
|
|
27390
|
+
"finished": "completed",
|
|
27391
|
+
}
|
|
27392
|
+
passthrough_rows: list[dict] = []
|
|
27393
|
+
merged_rows: list[dict] = []
|
|
27394
|
+
seen_identities: set[str] = set()
|
|
27395
|
+
for idx, item in enumerate(items):
|
|
27396
|
+
if isinstance(item, str):
|
|
27397
|
+
raw = {"content": item}
|
|
27398
|
+
elif isinstance(item, dict):
|
|
27399
|
+
raw = dict(item)
|
|
27400
|
+
else:
|
|
27401
|
+
raise ValueError(f"item {idx}: invalid type")
|
|
27402
|
+
key = trim(str(raw.get("key", "") or "").strip(), 120)
|
|
27403
|
+
if key.startswith("bb:"):
|
|
27404
|
+
passthrough_rows.append(raw)
|
|
27405
|
+
continue
|
|
27406
|
+
raw_content = str(raw.get("content", raw.get("text", raw.get("title", "")))).strip()
|
|
27407
|
+
content = normalize_work_text(raw_content) or raw_content
|
|
27408
|
+
if not content:
|
|
27409
|
+
continue
|
|
27410
|
+
normalized: dict[str, object] = {"content": content}
|
|
27411
|
+
raw_status = str(raw.get("status", raw.get("state", "")) or "").strip().lower()
|
|
27412
|
+
if raw_status:
|
|
27413
|
+
normalized["status"] = status_alias.get(raw_status, raw_status)
|
|
27414
|
+
owner = str(raw.get("owner", "") or "").strip().lower()
|
|
27415
|
+
if owner in {"manager", "explorer", "developer", "reviewer"}:
|
|
27416
|
+
normalized["owner"] = owner
|
|
27417
|
+
elif role_key == "manager" and owner == "":
|
|
27418
|
+
normalized["owner"] = role_key
|
|
27419
|
+
active_form = str(raw.get("activeForm", raw.get("active_form", "")) or "").strip()
|
|
27420
|
+
if active_form:
|
|
27421
|
+
normalized["activeForm"] = active_form
|
|
27422
|
+
identity = self._flat_todo_identity(normalized)
|
|
27423
|
+
if not identity:
|
|
27424
|
+
identity = f"ad-hoc:{idx}:{trim(content, 80)}"
|
|
27425
|
+
merged = dict(existing_by_identity.get(identity, {}))
|
|
27426
|
+
if "activeForm" not in normalized:
|
|
27427
|
+
merged.pop("activeForm", None)
|
|
27428
|
+
merged.update(normalized)
|
|
27429
|
+
if identity in seen_identities:
|
|
27430
|
+
continue
|
|
27431
|
+
seen_identities.add(identity)
|
|
27432
|
+
merged_rows.append(merged)
|
|
27433
|
+
return self.todo.update(preserved_system + passthrough_rows + merged_rows)
|
|
27434
|
+
|
|
26485
27435
|
def _merge_plan_worker_todo_items(self, items: list[dict], role: str = "") -> str:
|
|
26486
27436
|
if not isinstance(items, list):
|
|
26487
27437
|
raise ValueError("items must be array")
|
|
@@ -26499,6 +27449,12 @@ body{padding:18px}
|
|
|
26499
27449
|
for row in existing:
|
|
26500
27450
|
if not isinstance(row, dict):
|
|
26501
27451
|
continue
|
|
27452
|
+
row_kind = self._todo_row_kind(row)
|
|
27453
|
+
if row_kind == "system":
|
|
27454
|
+
preserved.append(dict(row))
|
|
27455
|
+
continue
|
|
27456
|
+
if row_kind != "plan_worker":
|
|
27457
|
+
continue
|
|
26502
27458
|
owner = str(row.get("owner", "") or "").strip().lower()
|
|
26503
27459
|
row_step_id = trim(str(row.get("parent_step_id", "") or ""), 20)
|
|
26504
27460
|
if owner in worker_owners and row_step_id == step_id:
|
|
@@ -26588,7 +27544,16 @@ body{padding:18px}
|
|
|
26588
27544
|
and str(row.get("status", "")).lower() != "completed"):
|
|
26589
27545
|
row.pop("parent_step_id", None) # Not for current step
|
|
26590
27546
|
|
|
26591
|
-
|
|
27547
|
+
# Insert merged_target_rows right after the active plan step's bb: row in preserved,
|
|
27548
|
+
# so subtasks appear nested under their parent step rather than at the list bottom.
|
|
27549
|
+
_step_key = str(active_step.get("key", "") or "")
|
|
27550
|
+
_insert_idx = len(preserved) # fallback: append at end
|
|
27551
|
+
if _step_key:
|
|
27552
|
+
for _i, _r in enumerate(preserved):
|
|
27553
|
+
if str(_r.get("key", "") or "") == _step_key:
|
|
27554
|
+
_insert_idx = _i + 1
|
|
27555
|
+
break
|
|
27556
|
+
final_rows = preserved[:_insert_idx] + passthrough_rows + merged_target_rows + preserved[_insert_idx:]
|
|
26592
27557
|
return self.todo.update(final_rows)
|
|
26593
27558
|
|
|
26594
27559
|
def _merge_owner_scoped_todo_items(self, items: list[dict], role: str = "") -> str:
|
|
@@ -26815,7 +27780,7 @@ body{padding:18px}
|
|
|
26815
27780
|
content = self._build_plan_todo_reminder_text(plan_step, missing_subtasks=missing_subtasks)
|
|
26816
27781
|
if not content:
|
|
26817
27782
|
return False
|
|
26818
|
-
self.
|
|
27783
|
+
self._append_plan_guidance_bubble(content, summary="todo reminder")
|
|
26819
27784
|
self.last_todo_reminder_ts = now_tick
|
|
26820
27785
|
self.todo_reminder_count += 1
|
|
26821
27786
|
self._emit(
|
|
@@ -26834,7 +27799,7 @@ body{padding:18px}
|
|
|
26834
27799
|
|
|
26835
27800
|
if not isinstance(plan_step, dict):
|
|
26836
27801
|
return []
|
|
26837
|
-
raw =
|
|
27802
|
+
raw = normalize_embedded_newlines(plan_step.get("full_content", "") or plan_step.get("content", "") or "")
|
|
26838
27803
|
if not raw.strip():
|
|
26839
27804
|
return []
|
|
26840
27805
|
lines = [trim(str(line or "").strip(), 300) for line in raw.replace("\r\n", "\n").split("\n")]
|
|
@@ -26964,6 +27929,136 @@ body{padding:18px}
|
|
|
26964
27929
|
self.todo.items = preserved + replacement
|
|
26965
27930
|
return True
|
|
26966
27931
|
|
|
27932
|
+
def _check_step_verified_tag(self, plan_step: dict, *, messages: list | None = None) -> bool:
|
|
27933
|
+
"""Return True if the agent has emitted <step-verified> in any assistant message
|
|
27934
|
+
since this plan step was activated (i.e., after plan_step['activated_at']).
|
|
27935
|
+
Pass messages=self.agent_messages for sync mode; defaults to self.messages."""
|
|
27936
|
+
activated_at = float(plan_step.get("activated_at", 0.0) or 0.0)
|
|
27937
|
+
msg_list = messages if messages is not None else self.messages
|
|
27938
|
+
for msg in reversed(msg_list):
|
|
27939
|
+
if not isinstance(msg, dict):
|
|
27940
|
+
continue
|
|
27941
|
+
msg_ts = float(msg.get("ts", 0.0) or 0.0)
|
|
27942
|
+
# Stop once we reach messages predating step activation
|
|
27943
|
+
if activated_at > 0 and msg_ts > 0 and msg_ts < activated_at:
|
|
27944
|
+
break
|
|
27945
|
+
if msg.get("role") == "assistant":
|
|
27946
|
+
content = str(msg.get("content", "") or "")
|
|
27947
|
+
if "<step-verified" in content:
|
|
27948
|
+
return True
|
|
27949
|
+
return False
|
|
27950
|
+
|
|
27951
|
+
def _single_mode_validation_gate(self, plan_step: dict, tool_results: list[dict]) -> bool:
|
|
27952
|
+
"""Gate passes when:
|
|
27953
|
+
1. Phase is research/design (no execution needed)
|
|
27954
|
+
2. Model emitted <step-verified/> since step activation
|
|
27955
|
+
3. Blackboard shows phase-appropriate accumulated evidence (has_write+has_exec for implement)
|
|
27956
|
+
4. Escape hatch: 10 consecutive blocks
|
|
27957
|
+
Research/design phases exempt. Escape hatch prevents permanent stall."""
|
|
27958
|
+
step_id = str(plan_step.get("id", "") or "")
|
|
27959
|
+
_flag = f"_smvg_{step_id}"
|
|
27960
|
+
if getattr(self, _flag, False):
|
|
27961
|
+
return True # Already validated in a previous round
|
|
27962
|
+
step_content = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "").lower()
|
|
27963
|
+
phase = self._plan_step_phase_hint(step_content)
|
|
27964
|
+
if phase in ("research", "design"):
|
|
27965
|
+
setattr(self, _flag, True)
|
|
27966
|
+
return True # No verification needed for non-execution phases
|
|
27967
|
+
# Escape hatch: after 10 consecutive blocks, unblock to prevent permanent stall
|
|
27968
|
+
_n_flag = f"_smvg_n_{step_id}"
|
|
27969
|
+
_n_blocked = int(getattr(self, _n_flag, 0))
|
|
27970
|
+
if _n_blocked >= 10:
|
|
27971
|
+
setattr(self, _flag, True)
|
|
27972
|
+
return True
|
|
27973
|
+
# Path A: model explicit tag
|
|
27974
|
+
if self._check_step_verified_tag(plan_step):
|
|
27975
|
+
setattr(self, _flag, True)
|
|
27976
|
+
return True
|
|
27977
|
+
# Path B: blackboard shows phase-appropriate accumulated evidence
|
|
27978
|
+
# implement: has_write AND (has_exec OR ...) — wrote files + ran bash
|
|
27979
|
+
# test/review: has_exec OR has_test_pass — ran tests
|
|
27980
|
+
# other: use generic evidence check
|
|
27981
|
+
if self._plan_step_has_blackboard_evidence(plan_step):
|
|
27982
|
+
setattr(self, _flag, True)
|
|
27983
|
+
return True
|
|
27984
|
+
# Gate blocked — increment counter and inject hint
|
|
27985
|
+
setattr(self, _n_flag, _n_blocked + 1)
|
|
27986
|
+
self._inject_single_mode_validation_hint(plan_step)
|
|
27987
|
+
return False
|
|
27988
|
+
|
|
27989
|
+
def _inject_single_mode_validation_hint(self, plan_step: dict):
|
|
27990
|
+
"""Inject a hint (rate-limited 20s) instructing the model to emit <step-verified/>
|
|
27991
|
+
after evaluating bash output against the step's acceptance criteria."""
|
|
27992
|
+
step_id = str(plan_step.get("id", "") or "")
|
|
27993
|
+
_ts_flag = f"_smvg_ts_{step_id}"
|
|
27994
|
+
_last_ts = float(getattr(self, _ts_flag, 0.0))
|
|
27995
|
+
if float(now_ts()) - _last_ts < 20.0:
|
|
27996
|
+
return
|
|
27997
|
+
setattr(self, _ts_flag, float(now_ts()))
|
|
27998
|
+
step_label = trim(str(plan_step.get("content", "") or ""), 80)
|
|
27999
|
+
full_content = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "")
|
|
28000
|
+
# Extract ACCEPTANCE criteria line if present
|
|
28001
|
+
acceptance = ""
|
|
28002
|
+
for line in full_content.splitlines():
|
|
28003
|
+
if line.strip().upper().startswith("ACCEPTANCE:"):
|
|
28004
|
+
acceptance = line.strip()[len("ACCEPTANCE:"):].strip()
|
|
28005
|
+
break
|
|
28006
|
+
phase = self._plan_step_phase_hint(full_content.lower())
|
|
28007
|
+
if phase == "test":
|
|
28008
|
+
action = "run the tests with bash and evaluate the results"
|
|
28009
|
+
else:
|
|
28010
|
+
action = "run the build/compile/run command with bash and evaluate the output"
|
|
28011
|
+
accept_line = f"\nAcceptance criteria: {acceptance}" if acceptance else ""
|
|
28012
|
+
msg = (
|
|
28013
|
+
f"<verification-required>\n"
|
|
28014
|
+
f"All subtasks for \"{step_label}\" are marked complete.{accept_line}\n"
|
|
28015
|
+
f"Before this step can advance, you must:\n"
|
|
28016
|
+
f"1. {action}\n"
|
|
28017
|
+
f"2. Review the bash output and confirm it meets the acceptance criteria\n"
|
|
28018
|
+
f"3. If it passes, emit exactly: <step-verified/>\n"
|
|
28019
|
+
f"4. If it fails, fix the issue and retry — do NOT emit <step-verified/> until resolved\n"
|
|
28020
|
+
f"</verification-required>"
|
|
28021
|
+
)
|
|
28022
|
+
_recent = self.messages[-5:]
|
|
28023
|
+
if not any("<verification-required>" in str(m.get("content", "") or "") for m in _recent if isinstance(m, dict)):
|
|
28024
|
+
self.messages.append({"role": "user", "content": msg, "ts": now_ts()})
|
|
28025
|
+
|
|
28026
|
+
def _inject_sync_mode_verification_hint(self, plan_step: dict, worker_step: dict):
|
|
28027
|
+
"""Inject a verification hint into agent_messages (rate-limited 30s) for sync mode.
|
|
28028
|
+
Instructs the worker to emit <step-verified/> after evaluating bash output."""
|
|
28029
|
+
step_id = str(plan_step.get("id", "") or "")
|
|
28030
|
+
_ts_flag = f"_sync_sv_ts_{step_id}"
|
|
28031
|
+
_last_ts = float(getattr(self, _ts_flag, 0.0))
|
|
28032
|
+
if float(now_ts()) - _last_ts < 30.0:
|
|
28033
|
+
return
|
|
28034
|
+
setattr(self, _ts_flag, float(now_ts()))
|
|
28035
|
+
step_label = trim(str(plan_step.get("content", "") or ""), 80)
|
|
28036
|
+
full_content = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "")
|
|
28037
|
+
acceptance = ""
|
|
28038
|
+
for line in full_content.splitlines():
|
|
28039
|
+
if line.strip().upper().startswith("ACCEPTANCE:"):
|
|
28040
|
+
acceptance = line.strip()[len("ACCEPTANCE:"):].strip()
|
|
28041
|
+
break
|
|
28042
|
+
phase = self._plan_step_phase_hint(full_content.lower())
|
|
28043
|
+
if phase == "test":
|
|
28044
|
+
action = "run the tests with bash and evaluate results"
|
|
28045
|
+
else:
|
|
28046
|
+
action = "run the build/compile command with bash and evaluate the output"
|
|
28047
|
+
accept_line = f"\nAcceptance criteria: {acceptance}" if acceptance else ""
|
|
28048
|
+
msg = (
|
|
28049
|
+
f"<verification-required>\n"
|
|
28050
|
+
f"All subtasks for \"{step_label}\" are marked complete.{accept_line}\n"
|
|
28051
|
+
f"Before this step can advance:\n"
|
|
28052
|
+
f"1. {action}\n"
|
|
28053
|
+
f"2. Review the output and confirm acceptance criteria are met\n"
|
|
28054
|
+
f"3. If it passes, emit exactly: <step-verified/>\n"
|
|
28055
|
+
f"4. If it fails, fix and retry — do NOT emit <step-verified/> until resolved\n"
|
|
28056
|
+
f"</verification-required>"
|
|
28057
|
+
)
|
|
28058
|
+
_recent = self.agent_messages[-5:]
|
|
28059
|
+
if not any("<verification-required>" in str(m.get("content", "") or "") for m in _recent if isinstance(m, dict)):
|
|
28060
|
+
self.agent_messages.append({"role": "user", "content": msg, "ts": now_ts()})
|
|
28061
|
+
|
|
26967
28062
|
def _single_agent_plan_step_check(self, tool_results: list[dict]):
|
|
26968
28063
|
"""In single-agent mode, check if current plan step should be advanced based on tool results."""
|
|
26969
28064
|
bb = self._ensure_blackboard()
|
|
@@ -26981,6 +28076,24 @@ body{padding:18px}
|
|
|
26981
28076
|
if not current:
|
|
26982
28077
|
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
26983
28078
|
return
|
|
28079
|
+
# When a new step is activated with no subtasks yet, require TodoWrite first
|
|
28080
|
+
_cur_step_id = str(current.get("id", "") or "")
|
|
28081
|
+
if _cur_step_id:
|
|
28082
|
+
_existing_subs = self._active_plan_worker_todo_rows(_cur_step_id, role="")
|
|
28083
|
+
if not _existing_subs:
|
|
28084
|
+
_step_label_s = trim(str(current.get("content", "") or ""), 60)
|
|
28085
|
+
_force_tw_msg = (
|
|
28086
|
+
f"<action-required>\n"
|
|
28087
|
+
f"Step \"{_step_label_s}\" has no subtasks yet. "
|
|
28088
|
+
f"Your FIRST action MUST be to call TodoWrite with "
|
|
28089
|
+
f"parent_step_id=\"{_cur_step_id}\" to create this step's subtasks "
|
|
28090
|
+
f"(e.g. N.1, N.2 ...) before executing any other work.\n"
|
|
28091
|
+
f"</action-required>"
|
|
28092
|
+
)
|
|
28093
|
+
_recent_msgs = self.messages[-4:]
|
|
28094
|
+
if not any("<action-required>" in str(m.get("content", "") or "") for m in _recent_msgs if isinstance(m, dict)):
|
|
28095
|
+
self._append_plan_guidance_bubble(_force_tw_msg, summary="action required: create subtasks first")
|
|
28096
|
+
return # Wait for TodoWrite before doing other checks
|
|
26984
28097
|
# Heuristic: check if tool results indicate step completion
|
|
26985
28098
|
step_content = str(current.get("full_content", "") or current.get("content", "") or "").lower()
|
|
26986
28099
|
phase = self._plan_step_phase_hint(step_content)
|
|
@@ -26996,25 +28109,29 @@ body{padding:18px}
|
|
|
26996
28109
|
validation_ok_blackboard = self._plan_step_has_blackboard_evidence(current, bb)
|
|
26997
28110
|
validation_ok = validation_ok_current or validation_ok_blackboard
|
|
26998
28111
|
bb_sig = self._plan_step_blackboard_signals(current, bb)
|
|
28112
|
+
todo_progress_signal = any(
|
|
28113
|
+
isinstance(r, dict) and r.get("ok", False)
|
|
28114
|
+
and str(r.get("name", "")) in ("TodoWrite", "TodoWriteRescue")
|
|
28115
|
+
for r in tool_results
|
|
28116
|
+
)
|
|
26999
28117
|
# Auto-advance conditions:
|
|
27000
28118
|
should_advance = False
|
|
28119
|
+
_gate_blocked = False # True when validation gate fired and blocked — no other path may advance
|
|
27001
28120
|
# Priority 1: Check if worker subtasks are all completed (most reliable signal)
|
|
27002
28121
|
subtasks_done = self._step_subtasks_all_completed(current)
|
|
27003
|
-
if subtasks_done
|
|
27004
|
-
|
|
27005
|
-
|
|
27006
|
-
|
|
27007
|
-
|
|
27008
|
-
|
|
27009
|
-
isinstance(r, dict) and r.get("ok", False)
|
|
27010
|
-
and str(r.get("name", "")) in ("TodoWrite", "TodoWriteRescue")
|
|
27011
|
-
for r in tool_results
|
|
27012
|
-
)
|
|
27013
|
-
if todo_progress_signal and self._step_has_accumulated_evidence(current, bb):
|
|
28122
|
+
if subtasks_done:
|
|
28123
|
+
# Validation gate: passes when model emitted <step-verified/>, blackboard has
|
|
28124
|
+
# phase-appropriate evidence, or escape hatch (10 blocks). Gate is the authoritative
|
|
28125
|
+
# "step done" check — no additional validation_ok required after gate passes.
|
|
28126
|
+
_gate_ok = self._single_mode_validation_gate(current, tool_results)
|
|
28127
|
+
if _gate_ok:
|
|
27014
28128
|
should_advance = True
|
|
28129
|
+
else:
|
|
28130
|
+
_gate_blocked = True # Gate blocked — disable ALL remaining advancement paths
|
|
27015
28131
|
# Priority 2: Phase-based heuristics — BUT gate by subtask completion when subtasks exist
|
|
27016
28132
|
# CRITICAL: A single write_file must NOT advance when 3+ subtasks remain
|
|
27017
|
-
|
|
28133
|
+
# Skipped when validation gate has blocked advancement (subtasks_done + gate failed)
|
|
28134
|
+
if not should_advance and not _gate_blocked:
|
|
27018
28135
|
_has_subtasks_s = bool(self._active_plan_worker_todo_rows(
|
|
27019
28136
|
str(current.get("id", "") or ""), role=""
|
|
27020
28137
|
))
|
|
@@ -27033,7 +28150,8 @@ body{padding:18px}
|
|
|
27033
28150
|
):
|
|
27034
28151
|
should_advance = True
|
|
27035
28152
|
# Also check if the agent explicitly mentioned step completion
|
|
27036
|
-
|
|
28153
|
+
# Also blocked by validation gate when subtasks_done path was blocked
|
|
28154
|
+
if not should_advance and not _gate_blocked:
|
|
27037
28155
|
# Check last assistant message for step completion signals
|
|
27038
28156
|
last_text = ""
|
|
27039
28157
|
for msg in reversed(self.messages[-3:]):
|
|
@@ -27041,7 +28159,8 @@ body{padding:18px}
|
|
|
27041
28159
|
last_text = str(msg.get("content", "") or "").lower()
|
|
27042
28160
|
break
|
|
27043
28161
|
step_done_signals = ("step completed", "步骤完成", "step done", "完成了", "已完成",
|
|
27044
|
-
"next step", "下一步", "proceed to step", "进入下一"
|
|
28162
|
+
"next step", "下一步", "proceed to step", "进入下一",
|
|
28163
|
+
"全部完成", "✅", "all subtasks")
|
|
27045
28164
|
if validation_ok and any(sig in last_text for sig in step_done_signals):
|
|
27046
28165
|
should_advance = True
|
|
27047
28166
|
if should_advance:
|
|
@@ -27054,6 +28173,23 @@ body{padding:18px}
|
|
|
27054
28173
|
else:
|
|
27055
28174
|
self._inject_rework_if_needed(current, {"tool_results": tool_results})
|
|
27056
28175
|
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
28176
|
+
if todo_progress_signal and not subtasks_done:
|
|
28177
|
+
step_rows = self._active_plan_worker_todo_rows(str(current.get("id", "") or ""), role="")
|
|
28178
|
+
next_row = next(
|
|
28179
|
+
(r for r in step_rows if str(r.get("status", "") or "").strip().lower() == "in_progress"),
|
|
28180
|
+
None,
|
|
28181
|
+
)
|
|
28182
|
+
focus_text = trim(str((next_row or {}).get("content", "") or "").strip(), 180)
|
|
28183
|
+
if focus_text:
|
|
28184
|
+
focus_msg = (
|
|
28185
|
+
"<todo-focus>"
|
|
28186
|
+
f"Continue ONLY the current in_progress subtask: {focus_text}. "
|
|
28187
|
+
"Do not branch away from the active plan step."
|
|
28188
|
+
"</todo-focus>"
|
|
28189
|
+
)
|
|
28190
|
+
recent = self.messages[-6:]
|
|
28191
|
+
if not any(str(msg.get("content", "") or "").strip() == focus_msg for msg in recent if isinstance(msg, dict)):
|
|
28192
|
+
self._append_plan_guidance_bubble(focus_msg, summary="todo focus: continue current subtask")
|
|
27057
28193
|
|
|
27058
28194
|
def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
|
|
27059
28195
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
@@ -27063,7 +28199,9 @@ body{padding:18px}
|
|
|
27063
28199
|
rows = []
|
|
27064
28200
|
for todo in todos:
|
|
27065
28201
|
s = todo.get("status", "pending")
|
|
27066
|
-
c = todo.get("content", "")
|
|
28202
|
+
c = normalize_embedded_newlines(todo.get("content", "") or "")
|
|
28203
|
+
if str(todo.get("category", "") or "") == "plan_step" and "\n" in c:
|
|
28204
|
+
c = c.split("\n", 1)[0].strip()
|
|
27067
28205
|
ev = todo.get("evidence", "")
|
|
27068
28206
|
af = {
|
|
27069
28207
|
"in_progress": self._ui_text("todo_working", content=c),
|
|
@@ -27076,12 +28214,9 @@ body{padding:18px}
|
|
|
27076
28214
|
if bool(self.runtime_reclassify_required):
|
|
27077
28215
|
return
|
|
27078
28216
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
27079
|
-
|
|
27080
|
-
has_plan_steps =
|
|
27081
|
-
|
|
27082
|
-
for t in (bb.get("project_todos", []) if isinstance(bb.get("project_todos"), list) else [])
|
|
27083
|
-
)
|
|
27084
|
-
if not self._is_multi_agent_mode() and not has_plan_steps:
|
|
28217
|
+
route_kind = self._todo_route_kind(board=bb)
|
|
28218
|
+
has_plan_steps = route_kind in {"plan_single", "plan_sync"}
|
|
28219
|
+
if route_kind == "pure_single":
|
|
27085
28220
|
return
|
|
27086
28221
|
self._init_project_todos(bb)
|
|
27087
28222
|
self._update_project_todo_status(bb)
|
|
@@ -27093,25 +28228,25 @@ body{padding:18px}
|
|
|
27093
28228
|
pass
|
|
27094
28229
|
system_rows = self._todo_project_rows_from_blackboard(bb)
|
|
27095
28230
|
existing = self.todo.snapshot()
|
|
28231
|
+
bridged_flat_rows = False
|
|
27096
28232
|
worker_rows: list[dict] = []
|
|
27097
28233
|
non_system_rows: list[dict] = []
|
|
27098
|
-
|
|
27099
|
-
|
|
27100
|
-
|
|
27101
|
-
|
|
27102
|
-
|
|
27103
|
-
|
|
27104
|
-
|
|
27105
|
-
|
|
27106
|
-
|
|
27107
|
-
|
|
27108
|
-
|
|
27109
|
-
|
|
27110
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
non_system_rows = []
|
|
28234
|
+
if route_kind == "plan_single":
|
|
28235
|
+
worker_rows = self._todo_route_rows(route_kind, rows=existing, board=bb)
|
|
28236
|
+
if not worker_rows:
|
|
28237
|
+
flat_rows = self._todo_route_rows("pure_single", rows=existing, board=bb)
|
|
28238
|
+
bridged_rows, bridged_flat_rows = self._bridge_flat_todos_to_active_plan_step(flat_rows, board=bb)
|
|
28239
|
+
if bridged_flat_rows:
|
|
28240
|
+
worker_rows = self._todo_route_rows(route_kind, rows=bridged_rows, board=bb)
|
|
28241
|
+
elif route_kind == "plan_sync":
|
|
28242
|
+
worker_rows = self._todo_route_rows(route_kind, rows=existing, board=bb)
|
|
28243
|
+
elif route_kind == "pure_sync":
|
|
28244
|
+
worker_rows = self._todo_route_rows(
|
|
28245
|
+
route_kind,
|
|
28246
|
+
rows=existing,
|
|
28247
|
+
role=self._todo_worker_role_hint(board=bb),
|
|
28248
|
+
board=bb,
|
|
28249
|
+
)
|
|
27115
28250
|
# Smart trim: keep all active (in_progress/pending) system rows,
|
|
27116
28251
|
# but only recent 3 completed system rows to save capacity for worker subtasks
|
|
27117
28252
|
active_system = [r for r in system_rows if r.get("status") != "completed"]
|
|
@@ -27175,6 +28310,11 @@ body{padding:18px}
|
|
|
27175
28310
|
todo_out = self.todo.update(merged)
|
|
27176
28311
|
except Exception:
|
|
27177
28312
|
return
|
|
28313
|
+
if bridged_flat_rows:
|
|
28314
|
+
self._emit(
|
|
28315
|
+
"status",
|
|
28316
|
+
{"summary": "flat todos attached to current plan step"},
|
|
28317
|
+
)
|
|
27178
28318
|
if todo_out != self.todo.no_changes_text() and reason:
|
|
27179
28319
|
self._emit(
|
|
27180
28320
|
"status",
|
|
@@ -27507,7 +28647,7 @@ body{padding:18px}
|
|
|
27507
28647
|
task_type = trim(str(row.get("task_type", "") or "").strip().lower(), 40)
|
|
27508
28648
|
if task_type in TASK_PROFILE_TYPES:
|
|
27509
28649
|
merged["task_type"] = task_type
|
|
27510
|
-
complexity =
|
|
28650
|
+
complexity = normalize_task_complexity(row.get("complexity", ""), default="")
|
|
27511
28651
|
if complexity in TASK_COMPLEXITY_LEVELS:
|
|
27512
28652
|
merged["complexity"] = complexity
|
|
27513
28653
|
scale = trim(str(row.get("scale_preference", "") or "").strip().lower(), 20)
|
|
@@ -27552,7 +28692,7 @@ body{padding:18px}
|
|
|
27552
28692
|
def _fallback_task_level_decision(self, goal_text: str) -> dict:
|
|
27553
28693
|
profile = self._infer_task_profile(goal_text)
|
|
27554
28694
|
task_type = str(profile.get("task_type", "general") or "general")
|
|
27555
|
-
complexity =
|
|
28695
|
+
complexity = normalize_task_complexity(profile.get("complexity", "simple"), default="simple")
|
|
27556
28696
|
low = str(goal_text or "").lower()
|
|
27557
28697
|
inherit_previous_state = False
|
|
27558
28698
|
if bool(self.runtime_goal_reset_pending):
|
|
@@ -27671,9 +28811,9 @@ body{padding:18px}
|
|
|
27671
28811
|
level = 3
|
|
27672
28812
|
if task_type == "simple_qa":
|
|
27673
28813
|
level = 1 if len(str(goal_text or "")) <= 180 else 2
|
|
27674
|
-
elif complexity
|
|
28814
|
+
elif task_complexity_rank(complexity) <= task_complexity_rank("simple") and task_type in {"general"}:
|
|
27675
28815
|
level = 2
|
|
27676
|
-
elif complexity
|
|
28816
|
+
elif task_complexity_rank(complexity) <= task_complexity_rank("moderate"):
|
|
27677
28817
|
level = 3
|
|
27678
28818
|
elif any(tok in low for tok in ("system-level", "系统级", "blackboard", "orchestrator", "内核", "基础设施")):
|
|
27679
28819
|
level = 5
|
|
@@ -27735,7 +28875,9 @@ body{padding:18px}
|
|
|
27735
28875
|
"SCALE PREFERENCE: Infer fast|balanced|thorough from user wording. "
|
|
27736
28876
|
"User-stated preference overrides your default strategy. "
|
|
27737
28877
|
"Budget controls internal depth/compactness, NOT early-stop messaging to user.\n\n"
|
|
27738
|
-
"
|
|
28878
|
+
"CRITICAL OUTPUT CONTRACT: You MUST output exactly one classify_task_level tool call and no plain-text answer. "
|
|
28879
|
+
"A prose-only response is invalid and will be discarded.\n"
|
|
28880
|
+
"The tool call must include concise judgement, inherit_previous_state, "
|
|
27739
28881
|
"and semantic_confidence (high|medium|low). "
|
|
27740
28882
|
"Use low confidence only when semantic ambiguity is substantial, then set low_confidence_reason briefly.\n"
|
|
27741
28883
|
f"{model_language_instruction(self.ui_language)}"
|
|
@@ -27747,6 +28889,28 @@ body{padding:18px}
|
|
|
27747
28889
|
)
|
|
27748
28890
|
return base
|
|
27749
28891
|
|
|
28892
|
+
def _extract_classify_task_level_row(self, response: dict | None) -> dict:
|
|
28893
|
+
if not isinstance(response, dict):
|
|
28894
|
+
return {}
|
|
28895
|
+
tool_calls = response.get("tool_calls", []) if isinstance(response.get("tool_calls", []), list) else []
|
|
28896
|
+
for tc in tool_calls:
|
|
28897
|
+
fn = tc.get("function", {}) if isinstance(tc, dict) else {}
|
|
28898
|
+
if str(fn.get("name", "") or "").strip() != "classify_task_level":
|
|
28899
|
+
continue
|
|
28900
|
+
args = fn.get("arguments", {}) if isinstance(fn, dict) else {}
|
|
28901
|
+
if isinstance(args, dict):
|
|
28902
|
+
return dict(args)
|
|
28903
|
+
if isinstance(args, str):
|
|
28904
|
+
parsed, _ = parse_tool_arguments_with_error(args)
|
|
28905
|
+
if isinstance(parsed, dict):
|
|
28906
|
+
return dict(parsed)
|
|
28907
|
+
content = str(response.get("content", "") or "").strip()
|
|
28908
|
+
if content:
|
|
28909
|
+
parsed, _ = parse_tool_arguments_with_error(content)
|
|
28910
|
+
if isinstance(parsed, dict) and parsed.get("level") is not None:
|
|
28911
|
+
return dict(parsed)
|
|
28912
|
+
return {}
|
|
28913
|
+
|
|
27750
28914
|
def _skill_aware_reeval_task_level(
|
|
27751
28915
|
self,
|
|
27752
28916
|
goal_text: str,
|
|
@@ -27940,7 +29104,7 @@ body{padding:18px}
|
|
|
27940
29104
|
if low_confidence_mode:
|
|
27941
29105
|
rule_profile = self._infer_task_profile(goal_text)
|
|
27942
29106
|
fallback_task_type = str(rule_profile.get("task_type", "general") or "general")
|
|
27943
|
-
fallback_complexity =
|
|
29107
|
+
fallback_complexity = normalize_task_complexity(rule_profile.get("complexity", "simple"), default="simple")
|
|
27944
29108
|
fallback_objective = trim(str(rule_profile.get("direct_objective", "") or ""), 800)
|
|
27945
29109
|
else:
|
|
27946
29110
|
board_now = self._ensure_blackboard()
|
|
@@ -27951,12 +29115,10 @@ body{padding:18px}
|
|
|
27951
29115
|
)
|
|
27952
29116
|
if fallback_task_type not in TASK_PROFILE_TYPES:
|
|
27953
29117
|
fallback_task_type = "general"
|
|
27954
|
-
fallback_complexity =
|
|
27955
|
-
|
|
27956
|
-
|
|
29118
|
+
fallback_complexity = normalize_task_complexity(
|
|
29119
|
+
self.runtime_task_complexity or board_profile.get("complexity", "simple") or "simple",
|
|
29120
|
+
default="simple",
|
|
27957
29121
|
)
|
|
27958
|
-
if fallback_complexity not in TASK_COMPLEXITY_LEVELS:
|
|
27959
|
-
fallback_complexity = "simple"
|
|
27960
29122
|
fallback_objective = trim(
|
|
27961
29123
|
str(self.runtime_direct_objective or board_profile.get("direct_objective", "") or "").strip(),
|
|
27962
29124
|
800,
|
|
@@ -27968,22 +29130,20 @@ body{padding:18px}
|
|
|
27968
29130
|
task_type = trim(str(row.get("task_type", "") or "").strip().lower(), 40)
|
|
27969
29131
|
if task_type not in TASK_PROFILE_TYPES:
|
|
27970
29132
|
task_type = fallback_task_type
|
|
27971
|
-
complexity =
|
|
27972
|
-
if complexity not in TASK_COMPLEXITY_LEVELS:
|
|
27973
|
-
complexity = fallback_complexity
|
|
29133
|
+
complexity = normalize_task_complexity(row.get("complexity", ""), default=fallback_complexity)
|
|
27974
29134
|
if explicit_complexity in TASK_COMPLEXITY_LEVELS:
|
|
27975
|
-
complexity = explicit_complexity
|
|
29135
|
+
complexity = normalize_task_complexity(explicit_complexity, default=fallback_complexity)
|
|
27976
29136
|
elif preserve_existing_complexity and previous_complexity in TASK_COMPLEXITY_LEVELS:
|
|
27977
|
-
complexity = previous_complexity
|
|
29137
|
+
complexity = normalize_task_complexity(previous_complexity, default=fallback_complexity)
|
|
27978
29138
|
low_confidence_reason = trim(str(row.get("low_confidence_reason", "") or "").strip(), 220)
|
|
27979
29139
|
judgement = trim(str(row.get("judgement", "") or "").strip(), 200) or "manager classified task level"
|
|
27980
29140
|
objective = trim(str(row.get("direct_objective", "") or "").strip(), 800)
|
|
27981
29141
|
if not objective:
|
|
27982
29142
|
objective = fallback_objective
|
|
27983
29143
|
_prev_level_val = int(getattr(self, '_prev_applied_task_level', 0) or 0)
|
|
27984
|
-
_complexity_floor =
|
|
27985
|
-
if _complexity_floor
|
|
27986
|
-
complexity =
|
|
29144
|
+
_complexity_floor = normalize_task_complexity(getattr(self, 'runtime_complexity_floor', '') or '', default="")
|
|
29145
|
+
if _complexity_floor in TASK_COMPLEXITY_LEVELS and task_complexity_rank(complexity) < task_complexity_rank(_complexity_floor):
|
|
29146
|
+
complexity = _complexity_floor
|
|
27987
29147
|
self.runtime_task_level = int(level)
|
|
27988
29148
|
self._prev_applied_task_level = int(level)
|
|
27989
29149
|
self.runtime_execution_mode = mode
|
|
@@ -28175,34 +29335,50 @@ body{padding:18px}
|
|
|
28175
29335
|
retries=max(1, int(MODEL_OUTPUT_RETRY_TIMES)),
|
|
28176
29336
|
media_inputs=media_inputs_round,
|
|
28177
29337
|
)
|
|
28178
|
-
|
|
28179
|
-
|
|
28180
|
-
|
|
28181
|
-
|
|
28182
|
-
|
|
28183
|
-
|
|
28184
|
-
|
|
28185
|
-
|
|
28186
|
-
|
|
28187
|
-
|
|
28188
|
-
|
|
28189
|
-
)
|
|
28190
|
-
|
|
28191
|
-
|
|
28192
|
-
|
|
28193
|
-
|
|
28194
|
-
|
|
28195
|
-
|
|
28196
|
-
|
|
28197
|
-
|
|
28198
|
-
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
29338
|
+
row = self._extract_classify_task_level_row(response)
|
|
29339
|
+
if not row:
|
|
29340
|
+
repair_prompt = (
|
|
29341
|
+
"Previous answer was invalid because it did not produce a valid classify_task_level tool call. "
|
|
29342
|
+
"Retry now. Output exactly one classify_task_level tool call and no prose."
|
|
29343
|
+
)
|
|
29344
|
+
repair_response = self._chat_with_same_model_retry(
|
|
29345
|
+
[
|
|
29346
|
+
{"role": "user", "content": prompt, "ts": now_ts()},
|
|
29347
|
+
{"role": "user", "content": repair_prompt, "ts": now_ts()},
|
|
29348
|
+
],
|
|
29349
|
+
tools=self._manager_task_classify_tools(),
|
|
29350
|
+
system=self._manager_classification_system_prompt(),
|
|
29351
|
+
max_tokens=220,
|
|
29352
|
+
think=False,
|
|
29353
|
+
stream_thinking=False,
|
|
29354
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
29355
|
+
pinned_selection=pinned_selection,
|
|
29356
|
+
context_label="manager classify repair",
|
|
29357
|
+
retries=1,
|
|
29358
|
+
media_inputs=media_inputs_round,
|
|
29359
|
+
)
|
|
29360
|
+
row = self._extract_classify_task_level_row(repair_response)
|
|
29361
|
+
if row:
|
|
29362
|
+
row["inherit_previous_state"] = _to_bool_like(
|
|
29363
|
+
row.get("inherit_previous_state", False),
|
|
29364
|
+
default=False,
|
|
29365
|
+
)
|
|
29366
|
+
row["semantic_confidence"] = self._normalize_semantic_confidence(
|
|
29367
|
+
row.get("semantic_confidence", "medium"),
|
|
29368
|
+
default="medium",
|
|
29369
|
+
)
|
|
29370
|
+
if str(row.get("semantic_confidence", "medium")) == "low":
|
|
29371
|
+
# Skill-aware re-evaluation before falling back to keyword heuristic
|
|
29372
|
+
reeval_row = self._skill_aware_reeval_task_level(goal_text, row, pinned_selection)
|
|
29373
|
+
fallback_row = self._fallback_task_level_decision(goal_text)
|
|
29374
|
+
merged = self._merge_task_decision_for_low_confidence(reeval_row, fallback_row)
|
|
29375
|
+
return merged
|
|
29376
|
+
row["source"] = "manager"
|
|
29377
|
+
return row
|
|
28202
29378
|
row = self._fallback_task_level_decision(goal_text)
|
|
28203
29379
|
row["source"] = "fallback-no-toolcall"
|
|
28204
29380
|
row["semantic_confidence"] = "low"
|
|
28205
|
-
row["low_confidence_reason"] = "manager classifier returned no valid tool call"
|
|
29381
|
+
row["low_confidence_reason"] = "manager classifier returned no valid classify_task_level tool call"
|
|
28206
29382
|
return row
|
|
28207
29383
|
|
|
28208
29384
|
# ------------------------------------------------------------------
|
|
@@ -29010,7 +30186,7 @@ body{padding:18px}
|
|
|
29010
30186
|
"reason": "conclusive-reply-detected",
|
|
29011
30187
|
"source": "fallback",
|
|
29012
30188
|
}
|
|
29013
|
-
if complexity
|
|
30189
|
+
if task_complexity_rank(complexity) <= task_complexity_rank("moderate") and task_type == "simple_code":
|
|
29014
30190
|
if has_error_log:
|
|
29015
30191
|
return {
|
|
29016
30192
|
"target": "developer",
|
|
@@ -29148,6 +30324,10 @@ body{padding:18px}
|
|
|
29148
30324
|
task_type_low = str(row.get("task_type", "") or "").strip().lower()
|
|
29149
30325
|
# 5a: Merge in-memory routes with persisted routes for detection
|
|
29150
30326
|
bb_for_routes = self._ensure_blackboard()
|
|
30327
|
+
current_progress_fp = self._watchdog_state_fingerprint(bb_for_routes)
|
|
30328
|
+
last_delegate = bb_for_routes.get("last_delegate", {}) if isinstance(bb_for_routes.get("last_delegate"), dict) else {}
|
|
30329
|
+
last_progress_fp = trim(str(last_delegate.get("progress_fp", "") or "").strip(), 80)
|
|
30330
|
+
no_progress_since_last_delegate = bool(last_progress_fp and last_progress_fp == current_progress_fp)
|
|
29151
30331
|
persisted_routes = bb_for_routes.get("persisted_manager_routes", [])
|
|
29152
30332
|
if not isinstance(persisted_routes, list):
|
|
29153
30333
|
persisted_routes = []
|
|
@@ -29159,22 +30339,16 @@ body{padding:18px}
|
|
|
29159
30339
|
if (
|
|
29160
30340
|
isinstance(deleg, dict)
|
|
29161
30341
|
and str(deleg.get("target", "") or "").strip().lower() == target
|
|
30342
|
+
and (
|
|
30343
|
+
not str(deleg.get("progress_fp", "") or "").strip()
|
|
30344
|
+
or str(deleg.get("progress_fp", "") or "").strip() == current_progress_fp
|
|
30345
|
+
)
|
|
29162
30346
|
and int(deleg.get("count", 0) or 0) >= 3
|
|
29163
30347
|
):
|
|
29164
|
-
|
|
29165
|
-
|
|
29166
|
-
|
|
29167
|
-
|
|
29168
|
-
f"Anti-stall: delegation to '{target}' repeated {deleg.get('count')} times with same instruction. "
|
|
29169
|
-
"Forcing finish to break loop."
|
|
29170
|
-
)
|
|
29171
|
-
else:
|
|
29172
|
-
row["target"] = alt_targets[0] if alt_targets else "developer"
|
|
29173
|
-
row["instruction"] = (
|
|
29174
|
-
f"Anti-stall: delegation to '{target}' repeated {deleg.get('count')} times. "
|
|
29175
|
-
f"Switching to {row['target']} with fresh approach."
|
|
29176
|
-
)
|
|
29177
|
-
row["reason"] = f"{row.get('reason', '')}|anti-stall-repeated-delegation"
|
|
30348
|
+
if not no_progress_since_last_delegate:
|
|
30349
|
+
continue
|
|
30350
|
+
row = self._manager_recovery_route_for_repeated_delegate(row, board=bb_for_routes)
|
|
30351
|
+
row["reason"] = trim(f"{row.get('reason', '')}|anti-stall-repeated-delegation", 600)
|
|
29178
30352
|
row["source"] = "anti-stall"
|
|
29179
30353
|
return row
|
|
29180
30354
|
if task_type_low in ("simple_code", "engineering") and target == "explorer":
|
|
@@ -29197,7 +30371,7 @@ body{padding:18px}
|
|
|
29197
30371
|
if target not in AGENT_ROLES:
|
|
29198
30372
|
return row
|
|
29199
30373
|
recent = [str(x.get("target", "") or "").strip().lower() for x in merged_routes[-4:]]
|
|
29200
|
-
if len(recent) >= 3 and recent[-1] == target and recent[-2] == target and recent[-3] == target:
|
|
30374
|
+
if no_progress_since_last_delegate and len(recent) >= 3 and recent[-1] == target and recent[-2] == target and recent[-3] == target:
|
|
29201
30375
|
board = bb_for_routes
|
|
29202
30376
|
low_reason = str(row.get("reason", "") or "").strip().lower()
|
|
29203
30377
|
if "summary" in low_reason and len(board.get("code_artifacts", {}) or {}) > 0:
|
|
@@ -29242,7 +30416,7 @@ body{padding:18px}
|
|
|
29242
30416
|
row["reason"] = f"{row.get('reason', '')}|anti-stall->developer-suggest"
|
|
29243
30417
|
row["source"] = "anti-stall"
|
|
29244
30418
|
return row
|
|
29245
|
-
if len(recent) == 4 and recent[0] == recent[2] and recent[1] == recent[3] and recent[0] != recent[1]:
|
|
30419
|
+
if no_progress_since_last_delegate and len(recent) == 4 and recent[0] == recent[2] and recent[1] == recent[3] and recent[0] != recent[1]:
|
|
29246
30420
|
board = bb_for_routes
|
|
29247
30421
|
if len(board.get("code_artifacts", {}) or {}) > 0:
|
|
29248
30422
|
row["target"] = "finish"
|
|
@@ -29323,9 +30497,7 @@ body{padding:18px}
|
|
|
29323
30497
|
task_type = trim(str(row.get("task_type", default_type) or "").strip().lower(), 40) or default_type
|
|
29324
30498
|
if task_type not in TASK_PROFILE_TYPES:
|
|
29325
30499
|
task_type = default_type
|
|
29326
|
-
complexity =
|
|
29327
|
-
if complexity not in TASK_COMPLEXITY_LEVELS:
|
|
29328
|
-
complexity = default_complexity
|
|
30500
|
+
complexity = normalize_task_complexity(row.get("complexity", default_complexity) or default_complexity, default=default_complexity)
|
|
29329
30501
|
scale_preference = trim(
|
|
29330
30502
|
str(row.get("scale_preference", profile.get("scale_preference", self.runtime_scale_preference)) or "").strip().lower(),
|
|
29331
30503
|
20,
|
|
@@ -30116,6 +31288,7 @@ body{padding:18px}
|
|
|
30116
31288
|
"instruction": instruction,
|
|
30117
31289
|
"reason": trim(str(route.get("reason", "") or "").strip(), 600),
|
|
30118
31290
|
"source": trim(str(route.get("source", "") or "").strip(), 40),
|
|
31291
|
+
"progress_fp": self._watchdog_state_fingerprint(board),
|
|
30119
31292
|
"task_level": int(task_level),
|
|
30120
31293
|
"execution_mode": execution_mode,
|
|
30121
31294
|
"task_type": task_type,
|
|
@@ -30200,8 +31373,9 @@ body{padding:18px}
|
|
|
30200
31373
|
profile["task_type"] = task_type
|
|
30201
31374
|
if complexity in TASK_COMPLEXITY_LEVELS:
|
|
30202
31375
|
# Floor protection: if plan mode set a floor, do not allow downgrade
|
|
30203
|
-
|
|
30204
|
-
|
|
31376
|
+
_route_complexity_floor = normalize_task_complexity(self.runtime_complexity_floor, default="")
|
|
31377
|
+
if _route_complexity_floor in TASK_COMPLEXITY_LEVELS and task_complexity_rank(complexity) < task_complexity_rank(_route_complexity_floor):
|
|
31378
|
+
complexity = _route_complexity_floor
|
|
30205
31379
|
profile["complexity"] = complexity
|
|
30206
31380
|
profile["scale_preference"] = scale_preference if scale_preference in TASK_SCALE_PREFERENCES else "balanced"
|
|
30207
31381
|
if objective:
|
|
@@ -30571,8 +31745,25 @@ body{padding:18px}
|
|
|
30571
31745
|
)
|
|
30572
31746
|
self._emit("status", {"summary": f"reviewer finish blocked: {gate_reason}"})
|
|
30573
31747
|
else:
|
|
31748
|
+
bb_finish = self._ensure_blackboard()
|
|
31749
|
+
profile_finish = self._ensure_blackboard_task_profile(bb_finish)
|
|
31750
|
+
exec_mode = normalize_execution_mode(
|
|
31751
|
+
profile_finish.get("execution_mode", self._effective_execution_mode()),
|
|
31752
|
+
default=self._effective_execution_mode(),
|
|
31753
|
+
)
|
|
30574
31754
|
approval_note = summary_arg or output or "finish tool acknowledged"
|
|
30575
|
-
|
|
31755
|
+
if exec_mode == EXECUTION_MODE_SYNC:
|
|
31756
|
+
self._blackboard_append_section(
|
|
31757
|
+
"execution_logs",
|
|
31758
|
+
role_key,
|
|
31759
|
+
(
|
|
31760
|
+
"finish requested but deferred: sync mode requires reviewer pass before approval.\n"
|
|
31761
|
+
f"summary: {approval_note}"
|
|
31762
|
+
),
|
|
31763
|
+
)
|
|
31764
|
+
self._emit("status", {"summary": "finish deferred: sync mode requires reviewer approval"})
|
|
31765
|
+
else:
|
|
31766
|
+
self._blackboard_mark_approved(approval_note, role_key)
|
|
30576
31767
|
if not ok and output:
|
|
30577
31768
|
self._blackboard_append_section(
|
|
30578
31769
|
"execution_logs",
|
|
@@ -30610,6 +31801,7 @@ body{padding:18px}
|
|
|
30610
31801
|
role_key = self._sanitize_agent_role(role)
|
|
30611
31802
|
status = str((step or {}).get("status", "") or "")
|
|
30612
31803
|
text = trim(str((step or {}).get("text", "") or "").strip(), BLACKBOARD_MAX_TEXT)
|
|
31804
|
+
tool_results = (step or {}).get("tool_results", []) if isinstance((step or {}).get("tool_results"), list) else []
|
|
30613
31805
|
if role_key and text:
|
|
30614
31806
|
board = self._ensure_blackboard()
|
|
30615
31807
|
board["last_worker_reply"] = {
|
|
@@ -30630,7 +31822,28 @@ body{padding:18px}
|
|
|
30630
31822
|
self._blackboard_set_status("REVIEWING")
|
|
30631
31823
|
if self._reviewer_deems_done(text):
|
|
30632
31824
|
self._blackboard_mark_approved(text, role_key)
|
|
30633
|
-
|
|
31825
|
+
explicit_todo_write = any(
|
|
31826
|
+
isinstance(item, dict) and str(item.get("name", "") or "") in {"TodoWrite", "TodoWriteRescue"}
|
|
31827
|
+
for item in tool_results
|
|
31828
|
+
)
|
|
31829
|
+
if role_key and not explicit_todo_write:
|
|
31830
|
+
source_text = text or self._latest_agent_assistant_text(role_key)
|
|
31831
|
+
if re.search(r"(?m)^\s*(?:[-*•>]+\s*)?\[(?: |>|x)\]\s+\S", source_text or ""):
|
|
31832
|
+
board = self._ensure_blackboard()
|
|
31833
|
+
step_id = trim(str((self._get_active_plan_step(board) or {}).get("id", "") or ""), 20)
|
|
31834
|
+
parsed_rows = extract_todo_rows_from_text(
|
|
31835
|
+
source_text,
|
|
31836
|
+
default_parent_step_id=step_id,
|
|
31837
|
+
limit=12,
|
|
31838
|
+
)
|
|
31839
|
+
if parsed_rows:
|
|
31840
|
+
merged = self._merge_todo_signal_rows(parsed_rows, role=role_key, board=board)
|
|
31841
|
+
if merged != self.todo.no_changes_text():
|
|
31842
|
+
self._emit(
|
|
31843
|
+
"status",
|
|
31844
|
+
{"summary": f"todo synced from canonical {role_key} text"},
|
|
31845
|
+
)
|
|
31846
|
+
for item in tool_results:
|
|
30634
31847
|
if isinstance(item, dict) and bool(item.get("bb_applied", False)):
|
|
30635
31848
|
continue
|
|
30636
31849
|
self._blackboard_update_from_tool_result(role_key, item)
|
|
@@ -31307,6 +32520,10 @@ body{padding:18px}
|
|
|
31307
32520
|
"TODO TRACKING (mandatory): "
|
|
31308
32521
|
"When a plan step is active, follow the current todo subtask order instead of inventing a parallel path. "
|
|
31309
32522
|
"After completing ONE subtask, call TodoWrite immediately — mark that subtask as 'completed' and move the next one to 'in_progress' before doing more work. "
|
|
32523
|
+
"Prefer TodoWrite items as objects with explicit fields: "
|
|
32524
|
+
"{content, status, owner?, parent_step_id?}. "
|
|
32525
|
+
"If you must use strings, use ONLY canonical prefixes: '[ ]', '[>]', '[x]'. "
|
|
32526
|
+
"Do not use emoji markers or free-form localized status labels in TodoWrite payloads. "
|
|
31310
32527
|
"Do not silently batch multiple subtasks and do not delay todo updates until the end of the step. "
|
|
31311
32528
|
"This manual update is critical because skill re-evaluation is triggered by actual todo progress. "
|
|
31312
32529
|
"EDIT METHODOLOGY (follow strictly): "
|
|
@@ -31423,35 +32640,9 @@ body{padding:18px}
|
|
|
31423
32640
|
content = str(item).strip()
|
|
31424
32641
|
owner = owner_hint
|
|
31425
32642
|
parent_step_id = active_step_id
|
|
31426
|
-
|
|
31427
|
-
|
|
31428
|
-
|
|
31429
|
-
# "⬜ task" / "[ ] task" / "[pending] task" / "[todo] task" → pending
|
|
31430
|
-
import re as _re_status
|
|
31431
|
-
_prefix_m = _re_status.match(
|
|
31432
|
-
r'^(?:'
|
|
31433
|
-
r'[\u2705\u2611]\s*' # ✅ ☑
|
|
31434
|
-
r'|\[x\]\s*|\[done\]\s*|\[completed\]\s*|\[finish(?:ed)?\]\s*'
|
|
31435
|
-
r'|\(done\)\s*|\(completed\)\s*|\(x\)\s*'
|
|
31436
|
-
r')',
|
|
31437
|
-
content, _re_status.IGNORECASE
|
|
31438
|
-
)
|
|
31439
|
-
_prefix_ip = _re_status.match(
|
|
31440
|
-
r'^(?:'
|
|
31441
|
-
r'[\u25b6\u25ba\u27a1]\s*' # ▶ ► ➡
|
|
31442
|
-
r'|\[>\]\s*|\[doing\]\s*|\[in.?progress\]\s*'
|
|
31443
|
-
r'|\(doing\)\s*|\(in.?progress\)\s*'
|
|
31444
|
-
r')',
|
|
31445
|
-
content, _re_status.IGNORECASE
|
|
31446
|
-
)
|
|
31447
|
-
if _prefix_m:
|
|
31448
|
-
status = "completed"
|
|
31449
|
-
content = content[_prefix_m.end():].strip()
|
|
31450
|
-
elif _prefix_ip:
|
|
31451
|
-
status = "in_progress"
|
|
31452
|
-
content = content[_prefix_ip.end():].strip()
|
|
31453
|
-
else:
|
|
31454
|
-
status = "pending"
|
|
32643
|
+
parsed_status, parsed_content = split_todo_status_text(content)
|
|
32644
|
+
status = parsed_status or "pending"
|
|
32645
|
+
content = parsed_content or content
|
|
31455
32646
|
content = normalize_work_text(content) or content
|
|
31456
32647
|
if not content:
|
|
31457
32648
|
continue
|
|
@@ -31478,13 +32669,40 @@ body{padding:18px}
|
|
|
31478
32669
|
if i >= in_progress_index:
|
|
31479
32670
|
r["status"] = "in_progress"
|
|
31480
32671
|
break
|
|
31481
|
-
|
|
32672
|
+
route_kind = self._todo_route_kind(role=owner_hint)
|
|
32673
|
+
if route_kind in {"plan_single", "plan_sync"}:
|
|
31482
32674
|
return self._merge_plan_worker_todo_items(clean_items, role=owner_hint)
|
|
31483
|
-
if
|
|
32675
|
+
if route_kind == "pure_sync":
|
|
31484
32676
|
return self._merge_owner_scoped_todo_items(clean_items, role=owner_hint)
|
|
31485
32677
|
return self.todo.update(clean_items)
|
|
31486
32678
|
|
|
31487
|
-
def
|
|
32679
|
+
def _todo_progress_signature(self, rows: list[dict] | None = None) -> list[tuple[str, str, str, str]]:
|
|
32680
|
+
items = rows if isinstance(rows, list) else self.todo.snapshot()
|
|
32681
|
+
sig: list[tuple[str, str, str, str]] = []
|
|
32682
|
+
for row in items:
|
|
32683
|
+
if not isinstance(row, dict):
|
|
32684
|
+
continue
|
|
32685
|
+
sig.append(
|
|
32686
|
+
(
|
|
32687
|
+
normalize_work_text(str(row.get("content", "") or "")).strip().lower(),
|
|
32688
|
+
str(row.get("status", "pending") or "pending").strip().lower(),
|
|
32689
|
+
str(row.get("owner", "") or "").strip().lower(),
|
|
32690
|
+
str(row.get("parent_step_id", "") or "").strip(),
|
|
32691
|
+
)
|
|
32692
|
+
)
|
|
32693
|
+
return sig
|
|
32694
|
+
|
|
32695
|
+
def _todo_progress_changed(self, before_rows: list[dict] | None, after_rows: list[dict] | None) -> bool:
|
|
32696
|
+
return self._todo_progress_signature(before_rows) != self._todo_progress_signature(after_rows)
|
|
32697
|
+
|
|
32698
|
+
def _analyze_todo_result(
|
|
32699
|
+
self,
|
|
32700
|
+
tool_name: str,
|
|
32701
|
+
output: str,
|
|
32702
|
+
*,
|
|
32703
|
+
before_rows: list[dict] | None = None,
|
|
32704
|
+
after_rows: list[dict] | None = None,
|
|
32705
|
+
) -> tuple[str, str]:
|
|
31488
32706
|
txt = str(output or "").strip()
|
|
31489
32707
|
low = txt.lower()
|
|
31490
32708
|
has_worker_rows = self._todo_runtime_has_worker_rows()
|
|
@@ -31805,6 +33023,36 @@ body{padding:18px}
|
|
|
31805
33023
|
if budget_forced
|
|
31806
33024
|
else ""
|
|
31807
33025
|
)
|
|
33026
|
+
# If in plan mode, include the current in-progress subtask and the <step-verified/> escape path
|
|
33027
|
+
plan_subtask_hint = ""
|
|
33028
|
+
try:
|
|
33029
|
+
bb = self._ensure_blackboard()
|
|
33030
|
+
todos = bb.get("project_todos", [])
|
|
33031
|
+
active_step = next(
|
|
33032
|
+
(t for t in todos if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
|
|
33033
|
+
None,
|
|
33034
|
+
)
|
|
33035
|
+
if active_step:
|
|
33036
|
+
step_id = str(active_step.get("id", "") or "")
|
|
33037
|
+
active_subtask = next(
|
|
33038
|
+
(
|
|
33039
|
+
t for t in todos
|
|
33040
|
+
if t.get("category") != "plan_step"
|
|
33041
|
+
and t.get("status") == "in_progress"
|
|
33042
|
+
and str(t.get("parent_step_id", "") or "") == step_id
|
|
33043
|
+
),
|
|
33044
|
+
None,
|
|
33045
|
+
)
|
|
33046
|
+
if active_subtask:
|
|
33047
|
+
subtask_text = trim(str(active_subtask.get("content", "") or ""), 120)
|
|
33048
|
+
plan_subtask_hint = (
|
|
33049
|
+
f"\n当前子任务: {subtask_text}\n"
|
|
33050
|
+
"如果此子任务需要视觉/浏览器验证而无法通过 bash 完整执行,"
|
|
33051
|
+
"请创建相关文件,通过代码审查确认实现正确,"
|
|
33052
|
+
"然后在回复中发出 <step-verified/> 并调用 TodoWrite 将子任务标记为 completed。"
|
|
33053
|
+
)
|
|
33054
|
+
except Exception:
|
|
33055
|
+
pass
|
|
31808
33056
|
self.messages.append(
|
|
31809
33057
|
{
|
|
31810
33058
|
"role": "user",
|
|
@@ -31813,6 +33061,7 @@ body{padding:18px}
|
|
|
31813
33061
|
f"系统检测到空动作回合(consecutive_empty_action={int(streak)})。"
|
|
31814
33062
|
"你刚才进行了深入思考,但没有输出任何最终结果或工具调用。"
|
|
31815
33063
|
f"{budget_note}"
|
|
33064
|
+
f"{plan_subtask_hint}"
|
|
31816
33065
|
"请结束思考,立即基于现有推导输出最终结论,或发起一个明确、可执行的工具调用。 "
|
|
31817
33066
|
"System notice: you returned thinking-only content without final answer or tool calls. "
|
|
31818
33067
|
"Stop internal reasoning now and immediately output either the final conclusion or exactly one "
|
|
@@ -31948,12 +33197,15 @@ body{padding:18px}
|
|
|
31948
33197
|
except Exception:
|
|
31949
33198
|
token_decoded = token
|
|
31950
33199
|
token_decoded = token_decoded.strip()
|
|
31951
|
-
|
|
31952
|
-
|
|
33200
|
+
for piece in split_structured_todo_content(token_decoded, limit=7):
|
|
33201
|
+
piece_text = str(piece or "").strip()
|
|
33202
|
+
if piece_text and piece_text not in out:
|
|
33203
|
+
out.append(piece_text)
|
|
31953
33204
|
if out:
|
|
31954
33205
|
return out[:7]
|
|
31955
33206
|
# Fallback: parse non-empty lines / bullets
|
|
31956
|
-
|
|
33207
|
+
normalized_text = normalize_embedded_newlines(text)
|
|
33208
|
+
for line in normalized_text.splitlines():
|
|
31957
33209
|
s = line.strip().strip(",")
|
|
31958
33210
|
s = re.sub(r"^[\-\*\d\.\)\s]+", "", s).strip()
|
|
31959
33211
|
if not s:
|
|
@@ -32311,20 +33563,16 @@ body{padding:18px}
|
|
|
32311
33563
|
)
|
|
32312
33564
|
return out
|
|
32313
33565
|
if name == "TodoWrite":
|
|
32314
|
-
# Protect plan_step todos: worker TodoWrite creates sub-items, not replacements
|
|
32315
33566
|
bb = self._ensure_blackboard()
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
for t in bb.get("project_todos", [])
|
|
32319
|
-
)
|
|
32320
|
-
if has_plan_steps:
|
|
33567
|
+
route_kind = self._todo_route_kind(role=str(role_key or ""), board=bb)
|
|
33568
|
+
if route_kind in {"plan_single", "plan_sync"}:
|
|
32321
33569
|
items = args.get("items", [])
|
|
32322
33570
|
if isinstance(items, list):
|
|
32323
33571
|
for item in items:
|
|
32324
33572
|
if isinstance(item, dict) and not item.get("key", "").startswith("bb:"):
|
|
32325
33573
|
item["owner"] = str(role_key or "developer")
|
|
32326
33574
|
result = self._merge_plan_worker_todo_items(items, role=str(role_key or "developer"))
|
|
32327
|
-
elif
|
|
33575
|
+
elif route_kind == "pure_sync":
|
|
32328
33576
|
items = args.get("items", [])
|
|
32329
33577
|
if isinstance(items, list):
|
|
32330
33578
|
for item in items:
|
|
@@ -32333,50 +33581,6 @@ body{padding:18px}
|
|
|
32333
33581
|
result = self._merge_owner_scoped_todo_items(items, role=str(role_key))
|
|
32334
33582
|
else:
|
|
32335
33583
|
result = self.todo.update(args["items"])
|
|
32336
|
-
# Fix 1: Auto-advance plan step when all subtasks are completed
|
|
32337
|
-
# This handles the case where the worker's last turn only calls TodoWrite
|
|
32338
|
-
# and _post_execution_plan_step_check would miss it due to no "real" tool evidence
|
|
32339
|
-
if has_plan_steps:
|
|
32340
|
-
try:
|
|
32341
|
-
_as = self._get_active_plan_step()
|
|
32342
|
-
if isinstance(_as, dict):
|
|
32343
|
-
_as_id = str(_as.get("id", "") or "")
|
|
32344
|
-
if _as_id and self._step_subtasks_all_completed(_as):
|
|
32345
|
-
_acc_ev = self._collect_accumulated_step_evidence(_as)
|
|
32346
|
-
if _acc_ev and _acc_ev != "accumulated-step-evidence":
|
|
32347
|
-
# Has real evidence — auto-advance
|
|
32348
|
-
self._advance_plan_step(
|
|
32349
|
-
evidence=_acc_ev or "subtasks-all-completed",
|
|
32350
|
-
actor=str(role_key or "developer"),
|
|
32351
|
-
)
|
|
32352
|
-
elif self._step_has_accumulated_evidence(_as, bb):
|
|
32353
|
-
self._advance_plan_step(
|
|
32354
|
-
evidence="subtasks-all-completed",
|
|
32355
|
-
actor=str(role_key or "developer"),
|
|
32356
|
-
)
|
|
32357
|
-
except Exception:
|
|
32358
|
-
pass
|
|
32359
|
-
# Fix 5b: TodoWrite loop detection — force-advance after 3 consecutive calls
|
|
32360
|
-
if has_plan_steps:
|
|
32361
|
-
try:
|
|
32362
|
-
_as5 = self._get_active_plan_step()
|
|
32363
|
-
if isinstance(_as5, dict):
|
|
32364
|
-
_as5_id = str(_as5.get("id", "") or "")
|
|
32365
|
-
if _as5_id:
|
|
32366
|
-
if not hasattr(self, '_todowrite_step_counter'):
|
|
32367
|
-
self._todowrite_step_counter = {}
|
|
32368
|
-
cnt = self._todowrite_step_counter.get(_as5_id, 0) + 1
|
|
32369
|
-
self._todowrite_step_counter[_as5_id] = cnt
|
|
32370
|
-
if (cnt >= 3
|
|
32371
|
-
and self._step_subtasks_all_completed(_as5)
|
|
32372
|
-
and self._step_has_accumulated_evidence(_as5, bb)):
|
|
32373
|
-
# Force advance — worker is stuck in a loop AND step has real evidence
|
|
32374
|
-
self._advance_plan_step(
|
|
32375
|
-
evidence="force-advance:todowrite-loop-detected",
|
|
32376
|
-
actor=str(role_key or "developer"),
|
|
32377
|
-
)
|
|
32378
|
-
except Exception:
|
|
32379
|
-
pass
|
|
32380
33584
|
# Step completion skill recheck: if any item just got marked completed, re-evaluate skills
|
|
32381
33585
|
# This fires in ALL modes (single/sync/plan) when developer writes todos
|
|
32382
33586
|
try:
|
|
@@ -33195,6 +34399,18 @@ body{padding:18px}
|
|
|
33195
34399
|
},
|
|
33196
34400
|
)
|
|
33197
34401
|
self._persist()
|
|
34402
|
+
_proc = getattr(self, "_running_bash_proc", None)
|
|
34403
|
+
if _proc is not None:
|
|
34404
|
+
try:
|
|
34405
|
+
if os.name == "posix":
|
|
34406
|
+
try:
|
|
34407
|
+
os.killpg(os.getpgid(_proc.pid), signal.SIGKILL)
|
|
34408
|
+
except Exception:
|
|
34409
|
+
_proc.kill()
|
|
34410
|
+
else:
|
|
34411
|
+
_proc.kill()
|
|
34412
|
+
except Exception:
|
|
34413
|
+
pass
|
|
33198
34414
|
|
|
33199
34415
|
def _reviewer_approval_log_gate(self, board: dict | None = None) -> tuple[bool, str]:
|
|
33200
34416
|
bb = board if isinstance(board, dict) else self._ensure_blackboard()
|
|
@@ -33599,8 +34815,8 @@ body{padding:18px}
|
|
|
33599
34815
|
isinstance(t, dict) and t.get("category") == "plan_step"
|
|
33600
34816
|
for t in board.get("project_todos", [])
|
|
33601
34817
|
)
|
|
33602
|
-
_sync_complexity =
|
|
33603
|
-
if not _sync_has_plan and _sync_complexity
|
|
34818
|
+
_sync_complexity = normalize_task_complexity(profile.get("complexity", "simple"), default="simple")
|
|
34819
|
+
if not _sync_has_plan and task_complexity_at_least(_sync_complexity, "moderate"):
|
|
33604
34820
|
self.messages.append({
|
|
33605
34821
|
"role": "system",
|
|
33606
34822
|
"content": (
|
|
@@ -33707,29 +34923,59 @@ body{padding:18px}
|
|
|
33707
34923
|
self._mark_all_done_silently(note)
|
|
33708
34924
|
self._emit("status", {"summary": "manager decided finish; run paused"})
|
|
33709
34925
|
break
|
|
33710
|
-
# Detect manager
|
|
34926
|
+
# Detect manager loop: same instruction repeated with unchanged progress.
|
|
33711
34927
|
import hashlib as _hl_mgr
|
|
33712
|
-
|
|
34928
|
+
_delegate_progress_fp = self._watchdog_state_fingerprint(self._ensure_blackboard())
|
|
34929
|
+
_cur_hash = _hl_mgr.sha1((target + "|" + instruction + "|" + _delegate_progress_fp).encode("utf-8")).hexdigest()[:12]
|
|
33713
34930
|
if _cur_hash == _prev_delegation_hash:
|
|
33714
34931
|
_repeat_delegation_count += 1
|
|
33715
34932
|
else:
|
|
33716
34933
|
_repeat_delegation_count = 0
|
|
33717
34934
|
_prev_delegation_hash = _cur_hash
|
|
33718
34935
|
if _repeat_delegation_count >= 3:
|
|
33719
|
-
self._emit("status", {"summary": f"manager stuck: repeated identical delegation x{_repeat_delegation_count + 1}; forcing advance"})
|
|
33720
34936
|
_bb_stuck = self._ensure_blackboard()
|
|
33721
34937
|
_stuck_step = next(
|
|
33722
34938
|
(t for t in _bb_stuck.get("project_todos", [])
|
|
33723
34939
|
if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
|
|
33724
34940
|
None,
|
|
33725
34941
|
)
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
self._mark_all_done_silently("manager stuck: repeated delegation break")
|
|
33731
|
-
break
|
|
34942
|
+
_step_note = trim(str((_stuck_step or {}).get("content", "") or ""), 200)
|
|
34943
|
+
route = self._manager_recovery_route_for_repeated_delegate(route, board=_bb_stuck)
|
|
34944
|
+
target = str(route.get("target", "") or "").strip().lower()
|
|
34945
|
+
instruction = trim(str(route.get("instruction", "") or "").strip(), 1400)
|
|
33732
34946
|
_repeat_delegation_count = 0
|
|
34947
|
+
_prev_delegation_hash = ""
|
|
34948
|
+
self._emit(
|
|
34949
|
+
"status",
|
|
34950
|
+
{
|
|
34951
|
+
"summary": (
|
|
34952
|
+
f"manager loop recovery: repeated identical delegation under unchanged progress; "
|
|
34953
|
+
f"rerouting to {target}"
|
|
34954
|
+
)
|
|
34955
|
+
},
|
|
34956
|
+
)
|
|
34957
|
+
self._append_manager_context(
|
|
34958
|
+
{
|
|
34959
|
+
"role": "system",
|
|
34960
|
+
"content": (
|
|
34961
|
+
"[manager-loop-guard] Repeated identical delegation detected under unchanged progress. "
|
|
34962
|
+
"Do NOT mark the active step completed just because the owner was delegated repeatedly. "
|
|
34963
|
+
"Use a recovery route based on current step evidence and worker todo state."
|
|
34964
|
+
+ (f" Active step: {_step_note}." if _step_note else "")
|
|
34965
|
+
+ (f" Recovery target: {target}." if target else "")
|
|
34966
|
+
),
|
|
34967
|
+
"ts": now_ts(),
|
|
34968
|
+
}
|
|
34969
|
+
)
|
|
34970
|
+
self._blackboard_append_section(
|
|
34971
|
+
"execution_logs",
|
|
34972
|
+
"manager",
|
|
34973
|
+
(
|
|
34974
|
+
"manager repeated identical delegation; applied recovery reroute"
|
|
34975
|
+
+ (f"\nactive_step: {_step_note}" if _step_note else "")
|
|
34976
|
+
+ (f"\nrecovery_target: {target}" if target else "")
|
|
34977
|
+
),
|
|
34978
|
+
)
|
|
33733
34979
|
role = self._sanitize_agent_role(target) or "developer"
|
|
33734
34980
|
self._inject_manager_instruction(
|
|
33735
34981
|
role,
|
|
@@ -33758,6 +35004,24 @@ body{padding:18px}
|
|
|
33758
35004
|
self._blackboard_update_from_worker_step(role, step)
|
|
33759
35005
|
# Post-execution plan step advancement (replaces pre-execution advancement)
|
|
33760
35006
|
self._post_execution_plan_step_check(route, step if isinstance(step, dict) else {})
|
|
35007
|
+
progress_capsule = self._manager_worker_progress_capsule(
|
|
35008
|
+
role,
|
|
35009
|
+
step if isinstance(step, dict) else {},
|
|
35010
|
+
self._ensure_blackboard(),
|
|
35011
|
+
)
|
|
35012
|
+
if progress_capsule:
|
|
35013
|
+
recent_mgr = self.manager_context[-4:] if isinstance(self.manager_context, list) else []
|
|
35014
|
+
if not any(
|
|
35015
|
+
isinstance(msg, dict) and str(msg.get("content", "") or "").strip() == progress_capsule
|
|
35016
|
+
for msg in recent_mgr
|
|
35017
|
+
):
|
|
35018
|
+
self._append_manager_context(
|
|
35019
|
+
{
|
|
35020
|
+
"role": "system",
|
|
35021
|
+
"content": progress_capsule,
|
|
35022
|
+
"ts": now_ts(),
|
|
35023
|
+
}
|
|
35024
|
+
)
|
|
33761
35025
|
# Fix 6b: Pure sync no-plan — read worker-done signal and notify manager
|
|
33762
35026
|
_bb_sync = self._ensure_blackboard()
|
|
33763
35027
|
if _bb_sync.pop("sync_worker_round_done", False):
|
|
@@ -34106,17 +35370,19 @@ body{padding:18px}
|
|
|
34106
35370
|
bb["plan"]["phase"] = "synthesis"
|
|
34107
35371
|
self.blackboard = bb
|
|
34108
35372
|
|
|
34109
|
-
# Synthesis with retry
|
|
35373
|
+
# Synthesis with retry + model fallback + deterministic fallback
|
|
34110
35374
|
proposal = None
|
|
34111
|
-
for _synth_attempt in range(
|
|
35375
|
+
for _synth_attempt in range(PLAN_MODE_SYNTHESIS_MAX_ATTEMPTS):
|
|
34112
35376
|
proposal = self._plan_mode_synthesize_proposal(pinned_selection)
|
|
34113
35377
|
if proposal and proposal.get("options"):
|
|
34114
35378
|
break
|
|
34115
|
-
if _synth_attempt
|
|
35379
|
+
if _synth_attempt < (PLAN_MODE_SYNTHESIS_MAX_ATTEMPTS - 1):
|
|
34116
35380
|
self._emit("status", {"summary": "plan-mode: synthesis retry"})
|
|
34117
35381
|
if not proposal or not proposal.get("options"):
|
|
34118
35382
|
# Last resort: minimal fallback with simpler prompt and higher token budget
|
|
34119
35383
|
proposal = self._synthesis_minimal_fallback(pinned_selection)
|
|
35384
|
+
if not proposal or not proposal.get("options"):
|
|
35385
|
+
proposal = self._synthesis_programmatic_fallback()
|
|
34120
35386
|
if not proposal or not proposal.get("options"):
|
|
34121
35387
|
self._emit("status", {"summary": "plan-mode: synthesis failed, falling back to direct execution"})
|
|
34122
35388
|
self.runtime_plan_mode_needed = False
|
|
@@ -34522,21 +35788,29 @@ body{padding:18px}
|
|
|
34522
35788
|
f"- Option A: Direct workaround — bypass the blocker with an alternative method\n"
|
|
34523
35789
|
f"- Option B: Different path — re-approach the goal from a completely different angle\n"
|
|
34524
35790
|
f"- Option C: Minimal viable + user action items — do what's possible now, list what the user needs to do manually\n\n"
|
|
34525
|
-
f"
|
|
35791
|
+
f"You MUST call the submit_plan_proposal tool exactly once with:\n"
|
|
34526
35792
|
f"- context: brief failure analysis (what was tried, what failed, why)\n"
|
|
34527
35793
|
f"- options: array of 3 options, each with id (A/B/C), title, summary, steps, pros, cons, risk\n"
|
|
34528
|
-
f"- recommended: id of the recommended option\n
|
|
35794
|
+
f"- recommended: id of the recommended option\n"
|
|
35795
|
+
f"Do NOT answer with prose-only markdown. A response without submit_plan_proposal tool call is invalid.\n\n"
|
|
34529
35796
|
f"{model_language_instruction(self.ui_language)}"
|
|
34530
35797
|
)
|
|
34531
35798
|
synthesis_ctx = [
|
|
34532
|
-
{
|
|
35799
|
+
{
|
|
35800
|
+
"role": "system",
|
|
35801
|
+
"content": (
|
|
35802
|
+
"You are a recovery planner analyzing execution failures and proposing alternative approaches. "
|
|
35803
|
+
"You MUST call submit_plan_proposal exactly once."
|
|
35804
|
+
),
|
|
35805
|
+
"ts": now_ts(),
|
|
35806
|
+
},
|
|
34533
35807
|
{"role": "user", "content": synthesis_prompt, "ts": now_ts()},
|
|
34534
35808
|
]
|
|
34535
35809
|
try:
|
|
34536
35810
|
response = self._chat_with_same_model_retry(
|
|
34537
35811
|
synthesis_ctx,
|
|
34538
35812
|
tools=self._plan_mode_synthesis_tools(),
|
|
34539
|
-
system="Generate a structured stall recovery plan.
|
|
35813
|
+
system="Generate a structured stall recovery plan. You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
|
|
34540
35814
|
max_tokens=STALL_PLAN_SYNTHESIS_MAX_TOKENS,
|
|
34541
35815
|
think=False,
|
|
34542
35816
|
stream_thinking=False,
|
|
@@ -34545,12 +35819,33 @@ body{padding:18px}
|
|
|
34545
35819
|
context_label="stall-plan synthesis",
|
|
34546
35820
|
retries=MODEL_OUTPUT_RETRY_TIMES,
|
|
34547
35821
|
)
|
|
34548
|
-
|
|
34549
|
-
|
|
34550
|
-
|
|
34551
|
-
|
|
34552
|
-
|
|
34553
|
-
|
|
35822
|
+
proposal = self._extract_plan_proposal_from_response(response)
|
|
35823
|
+
if proposal.get("options"):
|
|
35824
|
+
return proposal
|
|
35825
|
+
repair_response = self._chat_with_same_model_retry(
|
|
35826
|
+
synthesis_ctx + [
|
|
35827
|
+
{
|
|
35828
|
+
"role": "user",
|
|
35829
|
+
"content": (
|
|
35830
|
+
"Previous answer was invalid because it did not produce a valid submit_plan_proposal tool call. "
|
|
35831
|
+
"Retry now. Output exactly one submit_plan_proposal tool call and no prose."
|
|
35832
|
+
),
|
|
35833
|
+
"ts": now_ts(),
|
|
35834
|
+
}
|
|
35835
|
+
],
|
|
35836
|
+
tools=self._plan_mode_synthesis_tools(),
|
|
35837
|
+
system="You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
|
|
35838
|
+
max_tokens=STALL_PLAN_SYNTHESIS_MAX_TOKENS,
|
|
35839
|
+
think=False,
|
|
35840
|
+
stream_thinking=False,
|
|
35841
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
35842
|
+
pinned_selection=pinned_selection,
|
|
35843
|
+
context_label="stall-plan synthesis repair",
|
|
35844
|
+
retries=1,
|
|
35845
|
+
)
|
|
35846
|
+
proposal = self._extract_plan_proposal_from_response(repair_response)
|
|
35847
|
+
if proposal.get("options"):
|
|
35848
|
+
return proposal
|
|
34554
35849
|
except Exception as exc:
|
|
34555
35850
|
self._emit("status", {"summary": f"stall plan synthesis error: {exc}"})
|
|
34556
35851
|
return {}
|
|
@@ -34621,6 +35916,186 @@ body{padding:18px}
|
|
|
34621
35916
|
lines.append(f"- {trim(str(t), 100)}")
|
|
34622
35917
|
return "\n".join(lines)
|
|
34623
35918
|
|
|
35919
|
+
def _normalize_plan_proposal_option(self, raw: dict, *, fallback_id: str) -> dict | None:
|
|
35920
|
+
if not isinstance(raw, dict):
|
|
35921
|
+
return None
|
|
35922
|
+
opt_id = trim(str(raw.get("id", "") or fallback_id).strip().upper(), 8) or fallback_id
|
|
35923
|
+
title = trim(str(raw.get("title", "") or "").strip(), 200)
|
|
35924
|
+
summary = trim(str(raw.get("summary", "") or "").strip(), 600)
|
|
35925
|
+
steps_raw = raw.get("steps", [])
|
|
35926
|
+
steps: list[str] = []
|
|
35927
|
+
if isinstance(steps_raw, list):
|
|
35928
|
+
for item in steps_raw:
|
|
35929
|
+
text = normalize_embedded_newlines(str(item or "")).strip()
|
|
35930
|
+
if text:
|
|
35931
|
+
steps.append(trim(text, PLAN_STEP_FULL_CONTENT_MAX_CHARS))
|
|
35932
|
+
elif isinstance(steps_raw, str):
|
|
35933
|
+
text = normalize_embedded_newlines(steps_raw).strip()
|
|
35934
|
+
if text:
|
|
35935
|
+
steps.append(trim(text, PLAN_STEP_FULL_CONTENT_MAX_CHARS))
|
|
35936
|
+
pros = trim(str(raw.get("pros", "") or "").strip(), 400)
|
|
35937
|
+
cons = trim(str(raw.get("cons", "") or "").strip(), 400)
|
|
35938
|
+
risk = trim(str(raw.get("risk", "") or "").strip().lower(), 20)
|
|
35939
|
+
if risk not in {"low", "medium", "high"}:
|
|
35940
|
+
risk = "medium"
|
|
35941
|
+
if not title and summary:
|
|
35942
|
+
title = trim(summary.split("\n", 1)[0], 120)
|
|
35943
|
+
if not title and steps:
|
|
35944
|
+
title = trim(steps[0].split("\n", 1)[0], 120)
|
|
35945
|
+
if not summary and steps:
|
|
35946
|
+
summary = trim(steps[0], 300)
|
|
35947
|
+
if not steps:
|
|
35948
|
+
return None
|
|
35949
|
+
return {
|
|
35950
|
+
"id": opt_id,
|
|
35951
|
+
"title": title or f"Option {opt_id}",
|
|
35952
|
+
"summary": summary or title or f"Plan {opt_id}",
|
|
35953
|
+
"steps": steps,
|
|
35954
|
+
"pros": pros,
|
|
35955
|
+
"cons": cons,
|
|
35956
|
+
"risk": risk,
|
|
35957
|
+
}
|
|
35958
|
+
|
|
35959
|
+
def _normalize_plan_proposal_payload(self, raw: object) -> dict:
|
|
35960
|
+
src = raw if isinstance(raw, dict) else {}
|
|
35961
|
+
context = trim(str(src.get("context", "") or "").strip(), 2000)
|
|
35962
|
+
raw_options = src.get("options", [])
|
|
35963
|
+
if isinstance(raw_options, dict):
|
|
35964
|
+
raw_options = [raw_options]
|
|
35965
|
+
if not isinstance(raw_options, list):
|
|
35966
|
+
raw_options = []
|
|
35967
|
+
option_ids = ("A", "B", "C")
|
|
35968
|
+
options: list[dict] = []
|
|
35969
|
+
seen_ids: set[str] = set()
|
|
35970
|
+
for idx, item in enumerate(raw_options[: max(1, PLAN_MODE_MAX_OPTIONS * 2)]):
|
|
35971
|
+
normalized = self._normalize_plan_proposal_option(
|
|
35972
|
+
item,
|
|
35973
|
+
fallback_id=option_ids[min(idx, len(option_ids) - 1)],
|
|
35974
|
+
)
|
|
35975
|
+
if not normalized:
|
|
35976
|
+
continue
|
|
35977
|
+
opt_id = str(normalized.get("id", "") or "").strip().upper() or option_ids[min(idx, len(option_ids) - 1)]
|
|
35978
|
+
if opt_id in seen_ids:
|
|
35979
|
+
opt_id = option_ids[min(len(seen_ids), len(option_ids) - 1)]
|
|
35980
|
+
normalized["id"] = opt_id
|
|
35981
|
+
if opt_id in seen_ids:
|
|
35982
|
+
continue
|
|
35983
|
+
seen_ids.add(opt_id)
|
|
35984
|
+
options.append(normalized)
|
|
35985
|
+
if len(options) >= PLAN_MODE_MAX_OPTIONS:
|
|
35986
|
+
break
|
|
35987
|
+
recommended = trim(str(src.get("recommended", "") or "").strip().upper(), 8)
|
|
35988
|
+
valid_ids = {str(opt.get("id", "") or "").strip().upper() for opt in options}
|
|
35989
|
+
if recommended not in valid_ids:
|
|
35990
|
+
recommended = str(options[0].get("id", "A") or "A") if options else ""
|
|
35991
|
+
return {
|
|
35992
|
+
"context": context,
|
|
35993
|
+
"options": options,
|
|
35994
|
+
"recommended": recommended,
|
|
35995
|
+
}
|
|
35996
|
+
|
|
35997
|
+
def _parse_plan_proposal_from_text(self, text: str) -> dict:
|
|
35998
|
+
raw = str(text or "").strip()
|
|
35999
|
+
if not raw:
|
|
36000
|
+
return {}
|
|
36001
|
+
candidates: list[str] = [raw]
|
|
36002
|
+
fence_matches = re.findall(r"```(?:json)?\s*([\s\S]*?)```", raw, flags=re.IGNORECASE)
|
|
36003
|
+
for block in fence_matches:
|
|
36004
|
+
block_text = str(block or "").strip()
|
|
36005
|
+
if block_text:
|
|
36006
|
+
candidates.append(block_text)
|
|
36007
|
+
start = raw.find("{")
|
|
36008
|
+
end = raw.rfind("}")
|
|
36009
|
+
if start >= 0 and end > start:
|
|
36010
|
+
candidates.append(raw[start : end + 1].strip())
|
|
36011
|
+
for candidate in candidates:
|
|
36012
|
+
repaired = repair_truncated_json_object(candidate)
|
|
36013
|
+
for probe in [candidate, repaired]:
|
|
36014
|
+
if not probe:
|
|
36015
|
+
continue
|
|
36016
|
+
try:
|
|
36017
|
+
parsed = json.loads(probe)
|
|
36018
|
+
except Exception:
|
|
36019
|
+
continue
|
|
36020
|
+
if isinstance(parsed, list):
|
|
36021
|
+
parsed = {"context": "", "options": parsed, "recommended": ""}
|
|
36022
|
+
proposal = self._normalize_plan_proposal_payload(parsed)
|
|
36023
|
+
if proposal.get("options"):
|
|
36024
|
+
return proposal
|
|
36025
|
+
return {}
|
|
36026
|
+
|
|
36027
|
+
def _extract_plan_proposal_from_response(self, response: dict | None) -> dict:
|
|
36028
|
+
if not isinstance(response, dict):
|
|
36029
|
+
return {}
|
|
36030
|
+
tool_calls = response.get("tool_calls", [])
|
|
36031
|
+
if isinstance(tool_calls, list):
|
|
36032
|
+
for tc in tool_calls:
|
|
36033
|
+
if not isinstance(tc, dict):
|
|
36034
|
+
continue
|
|
36035
|
+
fn = tc.get("function", {}) if isinstance(tc.get("function"), dict) else {}
|
|
36036
|
+
if str(fn.get("name", "") or "").strip() != "submit_plan_proposal":
|
|
36037
|
+
continue
|
|
36038
|
+
args = fn.get("arguments", {})
|
|
36039
|
+
if isinstance(args, dict):
|
|
36040
|
+
proposal = self._normalize_plan_proposal_payload(args)
|
|
36041
|
+
if proposal.get("options"):
|
|
36042
|
+
return proposal
|
|
36043
|
+
elif isinstance(args, str):
|
|
36044
|
+
parsed, _ = parse_tool_arguments_with_error(args)
|
|
36045
|
+
proposal = self._normalize_plan_proposal_payload(parsed)
|
|
36046
|
+
if proposal.get("options"):
|
|
36047
|
+
return proposal
|
|
36048
|
+
return self._parse_plan_proposal_from_text(str(response.get("content", "") or ""))
|
|
36049
|
+
|
|
36050
|
+
def _synthesis_programmatic_fallback(self) -> dict:
|
|
36051
|
+
bb = self._ensure_blackboard()
|
|
36052
|
+
goal = trim(str(self.runtime_reclassify_goal or self._latest_user_goal_text() or ""), 1200)
|
|
36053
|
+
findings = bb.get("plan", {}).get("findings", []) if isinstance(bb.get("plan"), dict) else []
|
|
36054
|
+
finding_lines: list[str] = []
|
|
36055
|
+
for row in findings[:6]:
|
|
36056
|
+
if not isinstance(row, dict):
|
|
36057
|
+
continue
|
|
36058
|
+
content = trim(str(row.get("content", "") or "").strip(), 280)
|
|
36059
|
+
if content:
|
|
36060
|
+
finding_lines.append(content)
|
|
36061
|
+
context = trim(
|
|
36062
|
+
(
|
|
36063
|
+
"Fallback synthesis generated automatically from the user goal and current research findings. "
|
|
36064
|
+
+ (" | ".join(finding_lines[:3]) if finding_lines else goal)
|
|
36065
|
+
),
|
|
36066
|
+
1800,
|
|
36067
|
+
)
|
|
36068
|
+
detailed_steps = [
|
|
36069
|
+
"1. Scope and constraints\nClarify the exact deliverable, inputs, and acceptance criteria for this task.",
|
|
36070
|
+
"2. Core implementation\nBuild the main artifact for the request using the most direct workable path.",
|
|
36071
|
+
"3. Verification\nRun at least one observable validation and capture the result.",
|
|
36072
|
+
"4. Delivery report\nSummarize what was built, how to run it, and the key outputs.",
|
|
36073
|
+
]
|
|
36074
|
+
if finding_lines:
|
|
36075
|
+
detailed_steps = [
|
|
36076
|
+
"1. Review findings and lock scope\nUse the collected findings to define the exact execution boundary and required inputs.",
|
|
36077
|
+
"2. Prepare files and dependencies\nCreate or align the necessary files, folders, and runtime prerequisites for the task.",
|
|
36078
|
+
"3. Implement the main work\nExecute the core build/change/generation work for the requested output.",
|
|
36079
|
+
"4. Validate with observable evidence\nRun a concrete check and confirm the expected output, exit code, or rendered result.",
|
|
36080
|
+
"5. Generate delivery report\nSummarize what was built, how to run it, and the key outputs.",
|
|
36081
|
+
]
|
|
36082
|
+
proposal = {
|
|
36083
|
+
"context": context,
|
|
36084
|
+
"options": [
|
|
36085
|
+
{
|
|
36086
|
+
"id": "A",
|
|
36087
|
+
"title": "Direct Execution Plan",
|
|
36088
|
+
"summary": trim(goal or "Execute the requested task with a direct, verifiable plan.", 240),
|
|
36089
|
+
"steps": detailed_steps,
|
|
36090
|
+
"pros": "Deterministic fallback that keeps plan-mode available even when model synthesis formatting is unstable.",
|
|
36091
|
+
"cons": "Less tailored than a fully synthesized multi-option proposal.",
|
|
36092
|
+
"risk": "medium",
|
|
36093
|
+
}
|
|
36094
|
+
],
|
|
36095
|
+
"recommended": "A",
|
|
36096
|
+
}
|
|
36097
|
+
return self._normalize_plan_proposal_payload(proposal)
|
|
36098
|
+
|
|
34624
36099
|
def _plan_mode_synthesize_proposal(self, pinned_selection: str) -> dict:
|
|
34625
36100
|
bb = self._ensure_blackboard()
|
|
34626
36101
|
plan_data = bb.get("plan", {})
|
|
@@ -34656,10 +36131,11 @@ body{padding:18px}
|
|
|
34656
36131
|
f"## Research Findings\n{trim(findings_text, 6000)}\n\n"
|
|
34657
36132
|
f"{skills_section}"
|
|
34658
36133
|
f"## Instructions\n"
|
|
34659
|
-
f"
|
|
36134
|
+
f"You MUST call the submit_plan_proposal tool exactly once with:\n"
|
|
34660
36135
|
f"- context: brief background analysis\n"
|
|
34661
36136
|
f"- options: array of 1-{PLAN_MODE_MAX_OPTIONS} options, each with id (A/B/C), title, summary, steps, pros, cons, risk\n"
|
|
34662
|
-
f"- recommended: id of the recommended option\n
|
|
36137
|
+
f"- recommended: id of the recommended option\n"
|
|
36138
|
+
f"Do NOT answer with prose-only markdown. A response without submit_plan_proposal tool call is invalid.\n\n"
|
|
34663
36139
|
f"STEP QUALITY REQUIREMENTS:\n"
|
|
34664
36140
|
f"- Each step must be a concrete, actionable instruction (NOT vague like 'analyze reports')\n"
|
|
34665
36141
|
f"- Include specific file paths (e.g., 'Read uploaded/IEDM_.parsed.md to extract key findings')\n"
|
|
@@ -34731,7 +36207,11 @@ body{padding:18px}
|
|
|
34731
36207
|
response = self._chat_with_same_model_retry(
|
|
34732
36208
|
synthesis_ctx,
|
|
34733
36209
|
tools=self._plan_mode_synthesis_tools(),
|
|
34734
|
-
system=
|
|
36210
|
+
system=(
|
|
36211
|
+
"Generate a structured plan proposal. "
|
|
36212
|
+
"You MUST call submit_plan_proposal exactly once. "
|
|
36213
|
+
"Do not answer with plain text."
|
|
36214
|
+
),
|
|
34735
36215
|
max_tokens=PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS,
|
|
34736
36216
|
think=False,
|
|
34737
36217
|
stream_thinking=False,
|
|
@@ -34740,13 +36220,31 @@ body{padding:18px}
|
|
|
34740
36220
|
context_label="plan-mode synthesis",
|
|
34741
36221
|
retries=MODEL_OUTPUT_RETRY_TIMES,
|
|
34742
36222
|
)
|
|
34743
|
-
|
|
34744
|
-
|
|
34745
|
-
|
|
34746
|
-
|
|
34747
|
-
|
|
34748
|
-
|
|
34749
|
-
|
|
36223
|
+
proposal = self._extract_plan_proposal_from_response(response)
|
|
36224
|
+
if proposal.get("options"):
|
|
36225
|
+
return proposal
|
|
36226
|
+
repair_response = self._chat_with_same_model_retry(
|
|
36227
|
+
synthesis_ctx + [
|
|
36228
|
+
{
|
|
36229
|
+
"role": "user",
|
|
36230
|
+
"content": (
|
|
36231
|
+
"Previous answer was invalid because it did not produce a valid submit_plan_proposal tool call. "
|
|
36232
|
+
"Retry now. Output exactly one submit_plan_proposal tool call and no prose."
|
|
36233
|
+
),
|
|
36234
|
+
"ts": now_ts(),
|
|
36235
|
+
}
|
|
36236
|
+
],
|
|
36237
|
+
tools=self._plan_mode_synthesis_tools(),
|
|
36238
|
+
system="You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
|
|
36239
|
+
max_tokens=PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS,
|
|
36240
|
+
think=False,
|
|
36241
|
+
stream_thinking=False,
|
|
36242
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
36243
|
+
pinned_selection=pinned_selection,
|
|
36244
|
+
context_label="plan-mode synthesis repair",
|
|
36245
|
+
retries=1,
|
|
36246
|
+
)
|
|
36247
|
+
return self._extract_plan_proposal_from_response(repair_response)
|
|
34750
36248
|
|
|
34751
36249
|
def _synthesis_minimal_fallback(self, pinned_selection: str) -> dict:
|
|
34752
36250
|
"""Last-resort: ask model for a single simple plan with higher max_tokens."""
|
|
@@ -34758,33 +36256,63 @@ body{padding:18px}
|
|
|
34758
36256
|
for f in (findings[:5] if isinstance(findings, list) else [])
|
|
34759
36257
|
)
|
|
34760
36258
|
prompt = (
|
|
34761
|
-
f"Generate ONE simple plan for this task.
|
|
36259
|
+
f"Generate ONE simple plan for this task. You MUST call submit_plan_proposal with exactly 1 option.\n\n"
|
|
34762
36260
|
f"Task: {goal}\n\nFindings: {trim(findings_text, 3000)}\n\n"
|
|
34763
36261
|
f"Return a single option with id='A', title, summary, and 5-10 concrete steps.\n"
|
|
36262
|
+
f"Do NOT answer with prose-only markdown. A response without submit_plan_proposal tool call is invalid.\n"
|
|
34764
36263
|
f"{model_language_instruction(self.ui_language)}"
|
|
34765
36264
|
)
|
|
34766
36265
|
ctx = [
|
|
34767
|
-
{
|
|
36266
|
+
{
|
|
36267
|
+
"role": "system",
|
|
36268
|
+
"content": (
|
|
36269
|
+
"You MUST call submit_plan_proposal exactly once. "
|
|
36270
|
+
"Do not answer with plain text."
|
|
36271
|
+
),
|
|
36272
|
+
"ts": now_ts(),
|
|
36273
|
+
},
|
|
34768
36274
|
{"role": "user", "content": prompt, "ts": now_ts()},
|
|
34769
36275
|
]
|
|
34770
36276
|
try:
|
|
34771
36277
|
response = self._chat_with_same_model_retry(
|
|
34772
36278
|
ctx,
|
|
34773
36279
|
tools=self._plan_mode_synthesis_tools(),
|
|
34774
|
-
system="
|
|
36280
|
+
system="You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
|
|
34775
36281
|
max_tokens=6000,
|
|
34776
36282
|
think=False,
|
|
34777
36283
|
stream_thinking=False,
|
|
34778
36284
|
on_thinking_chunk=self._append_live_thinking,
|
|
34779
36285
|
pinned_selection=pinned_selection,
|
|
34780
36286
|
context_label="plan-mode minimal fallback",
|
|
34781
|
-
retries=
|
|
36287
|
+
retries=3,
|
|
36288
|
+
)
|
|
36289
|
+
proposal = self._extract_plan_proposal_from_response(response)
|
|
36290
|
+
if proposal.get("options"):
|
|
36291
|
+
return proposal
|
|
36292
|
+
repair_response = self._chat_with_same_model_retry(
|
|
36293
|
+
ctx + [
|
|
36294
|
+
{
|
|
36295
|
+
"role": "user",
|
|
36296
|
+
"content": (
|
|
36297
|
+
"Previous answer was invalid because it did not produce a valid submit_plan_proposal tool call. "
|
|
36298
|
+
"Retry now. Output exactly one submit_plan_proposal tool call and no prose."
|
|
36299
|
+
),
|
|
36300
|
+
"ts": now_ts(),
|
|
36301
|
+
}
|
|
36302
|
+
],
|
|
36303
|
+
tools=self._plan_mode_synthesis_tools(),
|
|
36304
|
+
system="You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
|
|
36305
|
+
max_tokens=6000,
|
|
36306
|
+
think=False,
|
|
36307
|
+
stream_thinking=False,
|
|
36308
|
+
on_thinking_chunk=self._append_live_thinking,
|
|
36309
|
+
pinned_selection=pinned_selection,
|
|
36310
|
+
context_label="plan-mode minimal fallback repair",
|
|
36311
|
+
retries=1,
|
|
34782
36312
|
)
|
|
34783
|
-
|
|
34784
|
-
|
|
34785
|
-
|
|
34786
|
-
if isinstance(args, dict) and args.get("options"):
|
|
34787
|
-
return dict(args)
|
|
36313
|
+
proposal = self._extract_plan_proposal_from_response(repair_response)
|
|
36314
|
+
if proposal.get("options"):
|
|
36315
|
+
return proposal
|
|
34788
36316
|
except Exception:
|
|
34789
36317
|
pass
|
|
34790
36318
|
return {}
|
|
@@ -34868,7 +36396,7 @@ body{padding:18px}
|
|
|
34868
36396
|
grouped_steps = self._group_plan_steps(raw_steps if isinstance(raw_steps, list) else [])
|
|
34869
36397
|
plan_todos: list[dict] = []
|
|
34870
36398
|
for i, step in enumerate(grouped_steps[:max(1, int(limit))]):
|
|
34871
|
-
step_text = trim(
|
|
36399
|
+
step_text = trim(normalize_embedded_newlines(step).strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
|
|
34872
36400
|
if not step_text:
|
|
34873
36401
|
continue
|
|
34874
36402
|
step_lines = step_text.split("\n")
|
|
@@ -34995,7 +36523,7 @@ body{padding:18px}
|
|
|
34995
36523
|
_mid_re_exec = _re_exec.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
|
|
34996
36524
|
for t in plan_todos:
|
|
34997
36525
|
idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
34998
|
-
full =
|
|
36526
|
+
full = normalize_embedded_newlines(t.get("full_content", "") or t.get("content", "")).strip()
|
|
34999
36527
|
# Normalize: split concatenated N.N sub-steps onto own lines
|
|
35000
36528
|
full = _mid_re_exec.sub(r"\n\1", full)
|
|
35001
36529
|
header = full.split("\n")[0] if "\n" in full else full
|
|
@@ -35157,7 +36685,7 @@ body{padding:18px}
|
|
|
35157
36685
|
# Phase 0: Normalize — split mid-string N.N onto own lines
|
|
35158
36686
|
normalized: list[str] = []
|
|
35159
36687
|
for s in raw_steps:
|
|
35160
|
-
text =
|
|
36688
|
+
text = normalize_embedded_newlines(s).strip()
|
|
35161
36689
|
if not text:
|
|
35162
36690
|
continue
|
|
35163
36691
|
fixed = mid_numbered_re.sub(r"\n\1", text)
|
|
@@ -35492,18 +37020,16 @@ body{padding:18px}
|
|
|
35492
37020
|
chosen_title = trim(str(chosen.get("title", "") or choice_id).strip(), 800)
|
|
35493
37021
|
chosen_summary = trim(str(chosen.get("summary", "") or "").strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
|
|
35494
37022
|
# Preserve current complexity unless the user explicitly changes it elsewhere.
|
|
35495
|
-
_current_complexity =
|
|
35496
|
-
|
|
35497
|
-
|
|
35498
|
-
|
|
35499
|
-
|
|
35500
|
-
).strip().lower(),
|
|
35501
|
-
20,
|
|
37023
|
+
_current_complexity = normalize_task_complexity(
|
|
37024
|
+
self.runtime_task_complexity
|
|
37025
|
+
or profile.get("complexity", judgement.get("complexity", ""))
|
|
37026
|
+
or "",
|
|
37027
|
+
default="",
|
|
35502
37028
|
)
|
|
35503
37029
|
if _current_complexity in TASK_COMPLEXITY_LEVELS:
|
|
35504
37030
|
self.runtime_task_complexity = _current_complexity
|
|
35505
37031
|
else:
|
|
35506
|
-
_current_complexity =
|
|
37032
|
+
_current_complexity = normalize_task_complexity(str(self.runtime_task_complexity or "").strip().lower(), default="")
|
|
35507
37033
|
self.runtime_complexity_floor = str(_current_complexity or "complex")
|
|
35508
37034
|
_plan_risk = self._resolve_plan_option_risk(chosen)
|
|
35509
37035
|
try:
|
|
@@ -36055,7 +37581,7 @@ body{padding:18px}
|
|
|
36055
37581
|
)
|
|
36056
37582
|
continue
|
|
36057
37583
|
stop_note = (
|
|
36058
|
-
"模型连续多轮仅输出思考而无动作,自动执行已熔断停止(fault_counter>=
|
|
37584
|
+
"模型连续多轮仅输出思考而无动作,自动执行已熔断停止(fault_counter>=15)。"
|
|
36059
37585
|
"请尝试拆分任务,或切换更强的推理模型后继续。"
|
|
36060
37586
|
)
|
|
36061
37587
|
raise CircuitBreakerTriggered(stop_note)
|
|
@@ -36402,6 +37928,7 @@ body{padding:18px}
|
|
|
36402
37928
|
self.current_phase = f"tool:{name}"
|
|
36403
37929
|
self.current_tool_name = name
|
|
36404
37930
|
round_tool_names.append(name)
|
|
37931
|
+
todo_rows_before = self.todo.snapshot() if name in {"TodoWrite", "TodoWriteRescue"} else None
|
|
36405
37932
|
args = tc["function"]["arguments"]
|
|
36406
37933
|
args_error = str(tc.get("args_error", "") or "").strip()
|
|
36407
37934
|
raw_args = tc.get("raw_arguments")
|
|
@@ -36566,15 +38093,41 @@ body{padding:18px}
|
|
|
36566
38093
|
recovery_retry_rounds = 0
|
|
36567
38094
|
if dispatched_name in {"TodoWrite", "TodoWriteRescue"}:
|
|
36568
38095
|
todo_attempted = True
|
|
36569
|
-
|
|
38096
|
+
todo_rows_after = self.todo.snapshot()
|
|
38097
|
+
state, reason = self._analyze_todo_result(
|
|
38098
|
+
dispatched_name,
|
|
38099
|
+
output,
|
|
38100
|
+
before_rows=todo_rows_before,
|
|
38101
|
+
after_rows=todo_rows_after,
|
|
38102
|
+
)
|
|
36570
38103
|
if state == "ok":
|
|
36571
38104
|
used_todo = True
|
|
36572
38105
|
self.todo_write_issue_count = 0
|
|
36573
38106
|
self.todo_last_issue = ""
|
|
38107
|
+
self._emit(
|
|
38108
|
+
"status",
|
|
38109
|
+
{"summary": f"todo updated ({trim(reason, 100)})"},
|
|
38110
|
+
)
|
|
36574
38111
|
else:
|
|
36575
38112
|
self.todo_write_issue_count += 1
|
|
36576
38113
|
self.todo_last_issue = reason
|
|
36577
|
-
|
|
38114
|
+
self._emit(
|
|
38115
|
+
"status",
|
|
38116
|
+
{
|
|
38117
|
+
"summary": (
|
|
38118
|
+
"todo update produced no progress "
|
|
38119
|
+
f"({trim(reason, 100)})"
|
|
38120
|
+
)
|
|
38121
|
+
},
|
|
38122
|
+
)
|
|
38123
|
+
repeat_no_progress = any(
|
|
38124
|
+
token in str(reason or "").lower()
|
|
38125
|
+
for token in ("repeated", "no progress", "without changing")
|
|
38126
|
+
)
|
|
38127
|
+
if self.todo_write_issue_count >= 2 and (
|
|
38128
|
+
not self._todo_runtime_has_worker_rows(single_role)
|
|
38129
|
+
or repeat_no_progress
|
|
38130
|
+
):
|
|
36578
38131
|
self._emit(
|
|
36579
38132
|
"status",
|
|
36580
38133
|
{
|
|
@@ -36601,6 +38154,22 @@ body{padding:18px}
|
|
|
36601
38154
|
"ok": not str(output).startswith("Error:"),
|
|
36602
38155
|
}
|
|
36603
38156
|
)
|
|
38157
|
+
# Update blackboard signals (step_files, execution_logs) for plan+single mode.
|
|
38158
|
+
# In plan+sync this is handled by _blackboard_update_from_worker_step, but in
|
|
38159
|
+
# plan+single there is no worker turn — we must update inline so that
|
|
38160
|
+
# _plan_step_has_blackboard_evidence() can see the evidence when the gate fires.
|
|
38161
|
+
try:
|
|
38162
|
+
self._blackboard_update_from_tool_result(
|
|
38163
|
+
"developer",
|
|
38164
|
+
{
|
|
38165
|
+
"name": dispatched_name or name,
|
|
38166
|
+
"args": args if isinstance(args, dict) else {},
|
|
38167
|
+
"output": trim(str(output or ""), 3000),
|
|
38168
|
+
"ok": not str(output).startswith("Error:"),
|
|
38169
|
+
},
|
|
38170
|
+
)
|
|
38171
|
+
except Exception:
|
|
38172
|
+
pass
|
|
36604
38173
|
# Failure ledger: record tool call and detect errors (single-agent, unified)
|
|
36605
38174
|
self._ledger_record_tool_call(name, args if isinstance(args, dict) else {})
|
|
36606
38175
|
_sa_ok = not str(output or "").startswith("Error")
|
|
@@ -36909,6 +38478,22 @@ body{padding:18px}
|
|
|
36909
38478
|
self.rounds_without_todo += 1
|
|
36910
38479
|
else:
|
|
36911
38480
|
self.rounds_without_todo += 1
|
|
38481
|
+
concrete_work_without_todo = (
|
|
38482
|
+
not used_todo
|
|
38483
|
+
and self._todo_runtime_has_worker_rows(single_role)
|
|
38484
|
+
and any(
|
|
38485
|
+
isinstance(r, dict)
|
|
38486
|
+
and r.get("ok", False)
|
|
38487
|
+
and str(r.get("name", "") or "") in {
|
|
38488
|
+
"write_file",
|
|
38489
|
+
"edit_file",
|
|
38490
|
+
"bash",
|
|
38491
|
+
"read_file",
|
|
38492
|
+
"write_to_blackboard",
|
|
38493
|
+
}
|
|
38494
|
+
for r in single_round_tool_results
|
|
38495
|
+
)
|
|
38496
|
+
)
|
|
36912
38497
|
if (
|
|
36913
38498
|
todo_attempted
|
|
36914
38499
|
and not used_todo
|
|
@@ -36933,18 +38518,25 @@ body{padding:18px}
|
|
|
36933
38518
|
now_tick = now_ts()
|
|
36934
38519
|
can_remind = (now_tick - self.last_todo_reminder_ts) >= 20
|
|
36935
38520
|
if can_remind and self.todo_reminder_count < 2:
|
|
36936
|
-
if
|
|
36937
|
-
self.
|
|
36938
|
-
|
|
36939
|
-
|
|
36940
|
-
|
|
36941
|
-
|
|
36942
|
-
|
|
38521
|
+
if concrete_work_without_todo:
|
|
38522
|
+
self._append_plan_guidance_bubble(
|
|
38523
|
+
"<reminder>Update your todos now: finish the current subtask in TodoWrite before moving on.</reminder>",
|
|
38524
|
+
summary="todo reminder",
|
|
38525
|
+
)
|
|
38526
|
+
self.last_todo_reminder_ts = now_tick
|
|
38527
|
+
self.todo_reminder_count += 1
|
|
38528
|
+
elif not self._todo_runtime_has_worker_rows(single_role) and self.rounds_without_todo >= 2:
|
|
38529
|
+
self._append_plan_guidance_bubble(
|
|
38530
|
+
"<reminder>Please call TodoWrite now to update the current subtask before continuing. If it fails/repeats, switch to TodoWriteRescue.</reminder>",
|
|
38531
|
+
summary="todo reminder",
|
|
36943
38532
|
)
|
|
36944
38533
|
self.last_todo_reminder_ts = now_tick
|
|
36945
38534
|
self.todo_reminder_count += 1
|
|
36946
38535
|
elif self._todo_should_block_auto_continue("") and self.rounds_without_todo >= 4:
|
|
36947
|
-
self.
|
|
38536
|
+
self._append_plan_guidance_bubble(
|
|
38537
|
+
"<reminder>Update your todos now: finish the current subtask in TodoWrite before moving on.</reminder>",
|
|
38538
|
+
summary="todo reminder",
|
|
38539
|
+
)
|
|
36948
38540
|
self.last_todo_reminder_ts = now_tick
|
|
36949
38541
|
self.todo_reminder_count += 1
|
|
36950
38542
|
if manual_compact:
|
|
@@ -37038,6 +38630,12 @@ body{padding:18px}
|
|
|
37038
38630
|
self._generate_run_completion_summary()
|
|
37039
38631
|
except Exception:
|
|
37040
38632
|
pass
|
|
38633
|
+
try:
|
|
38634
|
+
_applied_runtime_updates = self._apply_deferred_runtime_updates()
|
|
38635
|
+
for _note in _applied_runtime_updates[:6]:
|
|
38636
|
+
self._emit("status", {"summary": _note})
|
|
38637
|
+
except Exception:
|
|
38638
|
+
pass
|
|
37041
38639
|
self._emit("status", {"summary": "run finished"})
|
|
37042
38640
|
cb = self.run_finished_callback
|
|
37043
38641
|
if cb:
|
|
@@ -38995,7 +40593,7 @@ function renderLlmFields(provider){const container=E('llmFieldsContainer');if(!c
|
|
|
38995
40593
|
async function scanOllamaModels(){const urlEl=E('llmF_ollama_url');const sel=E('llmF_ollama_model');const hint=E('ollamaScanHint');const baseUrl=(urlEl?.value||'').trim()||'http://127.0.0.1:11434';if(hint)hint.textContent=t('llm_scanning');try{const res=await fetch('/api/ollama/models?base_url='+encodeURIComponent(baseUrl));const data=await res.json();if(!data.ok||!data.models?.length){if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'');return}if(sel){sel.innerHTML='';for(const m of data.models){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(data.models.length))}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
|
|
38996
40594
|
async function scanOpenAICompatModels(provider){const scanMap={openai_compat:{urlKey:'openai_url',modelKey:'openai_model',keyKey:'openai_key',defaultUrl:'https://api.openai.com/v1'},siliconflow:{urlKey:'siliconflow_url',modelKey:'siliconflow_model',keyKey:'siliconflow_key',defaultUrl:'https://api.siliconflow.cn/v1'},vllm:{urlKey:'vllm_url',modelKey:'vllm_model',keyKey:'vllm_key',defaultUrl:'http://localhost:8000/v1'},lmstudio:{urlKey:'lmstudio_url',modelKey:'lmstudio_model',keyKey:'lmstudio_key',defaultUrl:'http://localhost:1234/v1'},glm:{urlKey:'glm_url',modelKey:'glm_model',keyKey:'glm_key',defaultUrl:'https://open.bigmodel.cn/api/paas/v4'},kimi:{urlKey:'kimi_url',modelKey:'kimi_model',keyKey:'kimi_key',defaultUrl:'https://api.moonshot.cn/v1'},openrouter:{urlKey:'openrouter_url',modelKey:'openrouter_model',keyKey:'openrouter_key',defaultUrl:'https://openrouter.ai/api/v1'},custom_http:{urlKey:'custom_url',modelKey:'custom_model',keyKey:'custom_key',defaultUrl:''}};const normalizedProvider=String(provider||'openai_compat').trim()||'openai_compat';const meta=scanMap[normalizedProvider]||scanMap.openai_compat;const urlEl=E('llmF_'+meta.urlKey);const modelEl=E('llmF_'+meta.modelKey);const hint=E('localScanHint');const baseUrl=(urlEl?.value||'').trim()||meta.defaultUrl||'';const apiKey=(E('llmF_'+meta.keyKey)?.value||'').trim();if(hint)hint.textContent=t('llm_scanning');try{let url='/api/openai_compat/models?provider='+encodeURIComponent(normalizedProvider)+'&base_url='+encodeURIComponent(baseUrl);if(apiKey)url+='&api_key='+encodeURIComponent(apiKey);const res=await fetch(url);const data=await res.json();const models=Array.isArray(data.models)?data.models.filter(Boolean):[];if(!data.ok){if(hint)hint.textContent=t('llm_scan_error')+(data.error?' ('+data.error+')':'');return}if(models.length){if(modelEl&&!String(modelEl.value||'').trim())modelEl.value=models[0];if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(models.length))+': '+models.slice(0,3).join(', ');return}if(data.reachable){if(hint)hint.textContent=t('llm_scan_reachable_manual')+(data.error?' ('+data.error+')':'');return}if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'')}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
|
|
38997
40595
|
function collectLlmConfig(){const provider=E('llmProvider')?.value||'ollama';const config={provider:provider};if(provider==='ollama'){config.ollama_url=(E('llmF_ollama_url')?.value||'').trim()||'http://127.0.0.1:11434';config.ollama_model=E('llmF_ollama_model')?.value||''}else if(provider==='custom_http'){const fields=LLM_PROVIDER_FIELDS.custom_http;for(const f of fields){const el=E('llmF_'+f.key);if(!el)continue;if(f.type==='textarea'){config[f.key]=el.value.trim()}else if(f.key==='temperature'){const v=parseFloat(el.value);if(!isNaN(v))config[f.key]=v}else if(f.key==='request_timeout'){const v=parseInt(el.value,10);if(!isNaN(v)&&v>0)config[f.key]=v}else{config[f.key]=el.value.trim()}}}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){const el=E('llmF_'+f.key);if(el){const raw=el.value.trim();config[f.key]=(provider!=='custom_http'&&f.type==='url')?(raw||String(f.placeholder||'').trim()):raw}}}config.thinking_stream=E('llmF_thinking_stream')?.value==='true';return config}
|
|
38998
|
-
async function submitLlmConfig(){if(!S.activeId){showError(t('select_session_first'));return}const config=collectLlmConfig();try{const payload={filename:'LLM.config.json',mime:'application/json',content_b64:btoa(unescape(encodeURIComponent(JSON.stringify(config,null,2))))};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'))}else{showError('')}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true});E('llmConfigModal').style.display='none'}catch(err){showError(err.message||String(err))}}
|
|
40596
|
+
async function submitLlmConfig(){if(!S.activeId){showError(t('select_session_first'));return}const config=collectLlmConfig();try{const payload={filename:'LLM.config.json',mime:'application/json',content_b64:btoa(unescape(encodeURIComponent(JSON.stringify(config,null,2))))};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});const note=String(out?.note||out?.model_catalog?.note||'').trim();if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'))}else if(note){showError(note)}else{showError('')}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true});E('llmConfigModal').style.display='none'}catch(err){showError(err.message||String(err))}}
|
|
38999
40597
|
function openLlmConfigModal(){const modal=E('llmConfigModal');if(!modal)return;modal.style.display='flex';const prov=E('llmProvider');if(prov){renderLlmFields(prov.value)}}
|
|
39000
40598
|
const COMPACT_AUTO_REFRESH_COUNT=3;
|
|
39001
40599
|
const COMPACT_AUTO_REFRESH_INTERVAL_MS=260;
|
|
@@ -42020,7 +43618,7 @@ async function renameSession(){if(!S.activeId){showError(t('select_session_first
|
|
|
42020
43618
|
async function deleteSession(){if(!S.activeId){showError(t('select_session_first'));return}const deletingId=S.activeId;const ok=confirm(t('delete_confirm'));if(!ok)return;await api('/api/sessions/'+S.activeId,{method:'DELETE'});if(S.previewBySession&&deletingId){delete S.previewBySession[deletingId]}if(S.fileExplorerBySession&&deletingId){delete S.fileExplorerBySession[deletingId]}S.activeId=null;S.snap=null;if(S.es)S.es.close();renderPreviewTabs();renderPreviewVisibility();renderActivePreview(false);await refreshSessions();if(S.sessions.length)await selectSession(S.sessions[0].id)}
|
|
42021
43619
|
async function applyModel(){const sel=E('modelSelect');const btn=E('applyModelBtn');const model=sel?.value||'';if(!model){showError(t('no_model_selected'));return}if(S.staticMode&&S.frozen)resumeAutoUpdates();S.config=S.config||{};const prevModel=String(S.config.model||'');const prevSnapModel=String(S.snap?.model||'');const prevSnapCatalog=(S.snap&&typeof S.snap==='object')?S.snap.llm_model_catalog:undefined;try{S.config.model=model;if(S.snap&&typeof S.snap==='object'){S.snap.model=_modelNameFromSelection(model)||S.snap.model;if(!S.snap.llm_model_catalog||typeof S.snap.llm_model_catalog!=='object')S.snap.llm_model_catalog={};S.snap.llm_model_catalog.selected=model}renderModelControls();renderStats();if(S.snap)renderBoards();if(sel)sel.disabled=true;if(btn)btn.disabled=true;const path=S.activeId?('/api/sessions/'+S.activeId+'/config/model'):'/api/config/model';const changed=await api(path,{method:'POST',body:JSON.stringify({selection:model,model})});if(changed?.note)showError(changed.note);else showError('');if(!applyModelCatalog(changed)){const cat=await loadModelCatalog();if(!applyModelCatalog(cat)){S.config.model=String(changed?.selected||model||'').trim();renderModelControls()}}if(S.snap&&typeof S.snap==='object'){const selected=String(S.config?.model||model||'').trim();const modelName=_modelNameFromSelection(selected);if(modelName)S.snap.model=modelName;if(changed&&typeof changed==='object')S.snap.llm_model_catalog=changed;renderBoards()}scheduleSnapshot({forceFull:true,delayMs:40,allowWhenFrozen:true})}catch(err){S.config.model=prevModel;if(S.snap&&typeof S.snap==='object'){if(prevSnapModel)S.snap.model=prevSnapModel;if(prevSnapCatalog!==undefined)S.snap.llm_model_catalog=prevSnapCatalog;renderBoards()}renderModelControls();renderStats();showError(err.message||String(err))}finally{if(sel)sel.disabled=false;if(btn)btn.disabled=false}}
|
|
42022
43620
|
|
|
42023
|
-
async function uploadLlmConfigFile(file){try{if(!S.activeId){showError(t('select_session_first'));return}if(!file){return}const arr=await file.arrayBuffer();const payload={filename:'LLM.config.json',mime:file.type||'application/json',content_b64:ab2b64(arr)};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'));}else{showError('');const modal=E('llmConfigModal');if(modal)modal.style.display='none'}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}catch(err){showError(err.message||String(err))}}
|
|
43621
|
+
async function uploadLlmConfigFile(file){try{if(!S.activeId){showError(t('select_session_first'));return}if(!file){return}const arr=await file.arrayBuffer();const payload={filename:'LLM.config.json',mime:file.type||'application/json',content_b64:ab2b64(arr)};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});const note=String(out?.note||out?.model_catalog?.note||'').trim();if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'));}else{showError(note||'');const modal=E('llmConfigModal');if(modal)modal.style.display='none'}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}catch(err){showError(err.message||String(err))}}
|
|
42024
43622
|
async function sendMessage(){showError('');const t=E('prompt').value.trim();if(!t||!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();E('prompt').value='';try{await waitForPendingUploads();await api('/api/sessions/'+S.activeId+'/message',{method:'POST',body:JSON.stringify({content:t})});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:120,allowWhenFrozen:true})}}catch(err){showError(err.message)}}
|
|
42025
43623
|
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})}}
|
|
42026
43624
|
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})}}
|
|
@@ -50708,6 +52306,14 @@ class AppContext:
|
|
|
50708
52306
|
return started
|
|
50709
52307
|
|
|
50710
52308
|
def _on_session_run_finished(self, user_id: str, session_id: str):
|
|
52309
|
+
try:
|
|
52310
|
+
mgr = self.manager_for_user(user_id)
|
|
52311
|
+
sess = mgr.get(session_id)
|
|
52312
|
+
if sess and bool(getattr(sess, "_deferred_runtime_sync_requested", False)):
|
|
52313
|
+
mgr._sync_from_session(sess, apply_to_all=False)
|
|
52314
|
+
sess._deferred_runtime_sync_requested = False
|
|
52315
|
+
except Exception:
|
|
52316
|
+
pass
|
|
50711
52317
|
if not self.scheduler_limits_enabled():
|
|
50712
52318
|
return
|
|
50713
52319
|
started_rows: list[dict] = []
|
|
@@ -52319,9 +53925,26 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
52319
53925
|
if not selection:
|
|
52320
53926
|
return self._send_json({"error": "selection required"}, status=400)
|
|
52321
53927
|
model_override = payload.get("model_override")
|
|
53928
|
+
if bool(getattr(sess, "running", False)):
|
|
53929
|
+
try:
|
|
53930
|
+
sess._queue_deferred_runtime_update(
|
|
53931
|
+
"model_selection",
|
|
53932
|
+
{
|
|
53933
|
+
"selection": selection,
|
|
53934
|
+
"model_override": model_override if isinstance(model_override, str) else "",
|
|
53935
|
+
},
|
|
53936
|
+
)
|
|
53937
|
+
except Exception as exc:
|
|
53938
|
+
return self._send_json({"error": str(exc)}, status=400)
|
|
53939
|
+
queued = sess.model_catalog()
|
|
53940
|
+
queued["queued"] = True
|
|
53941
|
+
queued["note"] = (
|
|
53942
|
+
"session is running; model switch queued and will apply after the current run finishes"
|
|
53943
|
+
)
|
|
53944
|
+
return self._send_json(queued)
|
|
52322
53945
|
try:
|
|
52323
53946
|
out = sess.set_runtime_selection(selection, model_override if isinstance(model_override, str) else None)
|
|
52324
|
-
mgr._sync_from_session(sess, apply_to_all=
|
|
53947
|
+
mgr._sync_from_session(sess, apply_to_all=False)
|
|
52325
53948
|
except Exception as exc:
|
|
52326
53949
|
return self._send_json({"error": str(exc)}, status=400)
|
|
52327
53950
|
return self._send_json(out)
|
|
@@ -52420,9 +54043,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
52420
54043
|
if len(raw) > 20 * 1024 * 1024:
|
|
52421
54044
|
return self._send_json({"error": "max upload size is 20MB"}, status=413)
|
|
52422
54045
|
meta = sess.add_upload(filename, raw, mime)
|
|
52423
|
-
if isinstance(meta.get("model_catalog"), dict):
|
|
54046
|
+
if isinstance(meta.get("model_catalog"), dict) and not bool(meta.get("model_catalog", {}).get("queued")):
|
|
52424
54047
|
try:
|
|
52425
|
-
mgr._sync_from_session(sess, apply_to_all=
|
|
54048
|
+
mgr._sync_from_session(sess, apply_to_all=False)
|
|
52426
54049
|
except Exception:
|
|
52427
54050
|
pass
|
|
52428
54051
|
return self._send_json(meta, status=201)
|
|
@@ -52516,16 +54139,16 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
52516
54139
|
explicit_complexity = infer_user_complexity_value(
|
|
52517
54140
|
str(body.get("complexity", body.get("task_complexity", "")) or "")
|
|
52518
54141
|
)
|
|
52519
|
-
current_complexity =
|
|
52520
|
-
|
|
52521
|
-
|
|
54142
|
+
current_complexity = normalize_task_complexity(
|
|
54143
|
+
getattr(sess, "runtime_task_complexity", "") or "",
|
|
54144
|
+
default="",
|
|
52522
54145
|
)
|
|
52523
54146
|
if explicit_complexity in TASK_COMPLEXITY_LEVELS:
|
|
52524
|
-
sess.runtime_task_complexity = explicit_complexity
|
|
54147
|
+
sess.runtime_task_complexity = normalize_task_complexity(explicit_complexity, default="")
|
|
52525
54148
|
elif current_complexity in TASK_COMPLEXITY_LEVELS:
|
|
52526
54149
|
sess.runtime_task_complexity = current_complexity
|
|
52527
54150
|
else:
|
|
52528
|
-
sess.runtime_task_complexity =
|
|
54151
|
+
sess.runtime_task_complexity = normalize_task_complexity(policy.get("complexity", "simple"), default="simple")
|
|
52529
54152
|
sess.runtime_scale_preference = "thorough" if level >= 4 else "balanced"
|
|
52530
54153
|
return self._send_json({"task_level": level})
|
|
52531
54154
|
return self._send_json({"error": "not found"}, status=404)
|