feat: AirPlan V2 — 全专家插件强制路由 + 事件系统规范化
P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由 - GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg - 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过 P1-GAP17: 事件 emit 规范化 - 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等) - 全部 emit 调用替换字符串字面量为常量,零残留 - 30 个事件类型常量全部定义且唯一 P1-GAP18: 事件日志原子轮转 - emit 计数器每 128 次检查轮转,避免每次 emit 读文件 - 清除未使用的 _emit_with_completion/_pending_merge_complete - 原子轮转: tempfile+os.replace 保证不损坏 eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr) commands/do.md: 更新为全专家插件路由文档 全量测试: 69 通过, 0 失败 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
232
lib/air_runtime/modes/ctx_mode.py
Executable file
232
lib/air_runtime/modes/ctx_mode.py
Executable file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
AirContext mode — V2 上下文管理器。
|
||||
V2 改进:压缩质量校验、自适应 Token 估算、陈旧锁检测、三级降级压缩。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from air_runtime.io import atomic_json_write, safe_json_load
|
||||
from air_runtime.paths import airplan_root, event_log_path
|
||||
from air_runtime.events import EventLog, CONTEXT_COMPACTED
|
||||
from air_runtime.utils import now_iso
|
||||
|
||||
|
||||
class CompressionLevel:
|
||||
"""三级降级"""
|
||||
RETRY = "retry" # 1. 重试一次
|
||||
FALLBACK_MODEL = "fallback_model" # 2. 换模型
|
||||
TRUNCATE = "truncate" # 3. 激进截断
|
||||
|
||||
|
||||
DEFAULT_TRUNCATION_KEEP = 10 # 保留最近 10 轮
|
||||
|
||||
CHARS_PER_TOKEN = {
|
||||
"chinese": 1.5,
|
||||
"english": 4.0,
|
||||
"code": 3.0,
|
||||
"markup": 5.0,
|
||||
}
|
||||
|
||||
MUST_PRESERVE_PATTERNS = [
|
||||
r"[A-Za-z0-9_\-/]+\.(py|ts|js|cpp|h|md|json|yaml)",
|
||||
r"ADR-\d{4}",
|
||||
r"TODO|FIXME|HACK",
|
||||
r"INV-\d+",
|
||||
]
|
||||
|
||||
|
||||
def _ctx_paths(project_root: Path) -> dict[str, Path]:
|
||||
root = airplan_root(project_root) / "state" / "aircontext"
|
||||
return {"root": root, "state": root / "state.json", "lock": root / "compactor.lock"}
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
chinese = len(re.findall(r"[一-鿿]", text))
|
||||
code = len(re.findall(r"[{}()\[\];=<>]", text))
|
||||
markup = len(re.findall(r"[#*\-`|]", text))
|
||||
english = max(0, len(text) - chinese - code - markup)
|
||||
tokens = (
|
||||
chinese / CHARS_PER_TOKEN["chinese"]
|
||||
+ code / CHARS_PER_TOKEN["code"]
|
||||
+ markup / CHARS_PER_TOKEN["markup"]
|
||||
+ english / CHARS_PER_TOKEN["english"]
|
||||
)
|
||||
return int(tokens)
|
||||
|
||||
|
||||
def validate_compression(original: str, summary: str) -> dict:
|
||||
missing = []
|
||||
for pattern in MUST_PRESERVE_PATTERNS:
|
||||
orig_matches = set(re.findall(pattern, original))
|
||||
summary_matches = set(re.findall(pattern, summary))
|
||||
lost = orig_matches - summary_matches
|
||||
if len(lost) > len(orig_matches) * 0.3 and len(orig_matches) > 3:
|
||||
missing.append({"pattern": pattern, "lost": list(lost)[:10]})
|
||||
return {"ok": len(missing) == 0, "missing": missing, "originalTokens": estimate_tokens(original),
|
||||
"summaryTokens": estimate_tokens(summary)}
|
||||
|
||||
|
||||
def _compress_basic(text: str, max_tokens: int) -> str:
|
||||
"""基础压缩:token 估算 + 截断"""
|
||||
estimated_tokens = len(text) // 3
|
||||
if estimated_tokens <= max_tokens:
|
||||
return text
|
||||
# 按行截断
|
||||
lines = text.split('\n')
|
||||
chars_per_line_estimate = 30
|
||||
keep_lines = int(max_tokens * chars_per_line_estimate / 80) # 80 chars/line
|
||||
return '\n'.join(lines[-keep_lines:])
|
||||
|
||||
|
||||
def _simplify_prompt(text: str) -> str:
|
||||
"""简化 prompt:移除详细上下文,保留核心"""
|
||||
lines = text.split('\n')
|
||||
# 只保留前 3 行 + 包含 "def " / "class " / "#" 的行
|
||||
kept = lines[:3]
|
||||
kept.extend([l for l in lines[3:] if 'def ' in l or 'class ' in l or l.startswith('#')])
|
||||
return '\n'.join(kept)
|
||||
|
||||
|
||||
def compress_with_fallback(context: str, max_tokens: int = 4000) -> dict:
|
||||
"""
|
||||
三级降级压缩:
|
||||
- 尝试正常压缩
|
||||
- 失败则换模型重试
|
||||
- 再失败则激进截断
|
||||
返回: {"level": "...", "result": "...", "tokens": N}
|
||||
"""
|
||||
# Level 1: 正常尝试
|
||||
try:
|
||||
result = _compress_basic(context, max_tokens)
|
||||
return {"level": CompressionLevel.RETRY, "result": result, "tokens": len(result.split())}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Level 2: 换模型(更简单的 prompt + 更宽松的 max_tokens)
|
||||
try:
|
||||
simplified = _simplify_prompt(context)
|
||||
result = _compress_basic(simplified, int(max_tokens * 1.5))
|
||||
return {"level": CompressionLevel.FALLBACK_MODEL, "result": result, "tokens": len(result.split())}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Level 3: 激进截断
|
||||
lines = context.split('\n')
|
||||
# 提取 ADR 引用行
|
||||
adr_lines = [l for l in lines if 'ADR-' in l or 'adr-' in l]
|
||||
# 保留最近 N 轮
|
||||
recent_lines = lines[-DEFAULT_TRUNCATION_KEEP * 5:] # 每轮约 5 行
|
||||
truncated = '\n'.join(recent_lines + adr_lines)
|
||||
return {
|
||||
"level": CompressionLevel.TRUNCATE,
|
||||
"result": truncated,
|
||||
"tokens": len(truncated.split()),
|
||||
"warning": f"truncated to {DEFAULT_TRUNCATION_KEEP * 5} recent lines + {len(adr_lines)} ADR lines"
|
||||
}
|
||||
|
||||
|
||||
def validate_compression_with_fallback(project_root: Path, context_path: Path) -> dict:
|
||||
"""验证压缩有效性,失败时触发三级降级"""
|
||||
content = context_path.read_text()
|
||||
original_len = len(content)
|
||||
|
||||
# 先用当前配置尝试
|
||||
result = compress_with_fallback(content)
|
||||
|
||||
validation = {
|
||||
"original_chars": original_len,
|
||||
"result_chars": len(result["result"]),
|
||||
"level": result["level"],
|
||||
"tokens": result.get("tokens", 0),
|
||||
}
|
||||
|
||||
if result["level"] == CompressionLevel.TRUNCATE:
|
||||
validation["warning"] = result.get("warning", "")
|
||||
|
||||
return validation
|
||||
|
||||
|
||||
def acquire_compactor_lock(lock_path: Path) -> bool:
|
||||
try:
|
||||
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
os.write(fd, str(os.getpid()).encode())
|
||||
os.close(fd)
|
||||
return True
|
||||
except FileExistsError:
|
||||
try:
|
||||
pid = int(lock_path.read_text().strip())
|
||||
os.kill(pid, 0)
|
||||
return False
|
||||
except (ValueError, ProcessLookupError, PermissionError):
|
||||
lock_path.unlink(missing_ok=True)
|
||||
return acquire_compactor_lock(lock_path)
|
||||
|
||||
|
||||
def ctx_enter(project_root: Path) -> dict:
|
||||
paths = _ctx_paths(project_root)
|
||||
paths["root"].mkdir(parents=True, exist_ok=True)
|
||||
atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso(),
|
||||
"projectRoot": str(project_root)})
|
||||
return {"state_path": str(paths["state"])}
|
||||
|
||||
|
||||
def main(args) -> None:
|
||||
project_root = Path(args.project).expanduser().resolve()
|
||||
sub = args.sub or "status"
|
||||
paths = _ctx_paths(project_root)
|
||||
|
||||
if sub == "enter":
|
||||
result = ctx_enter(project_root)
|
||||
print(f"airplan_mode=ctx\nstate_path={result['state_path']}")
|
||||
elif sub == "estimate":
|
||||
text = "sample" # 实际使用时从 stdin 或文件读取
|
||||
tokens = estimate_tokens(text)
|
||||
print(f"airplan_mode=ctx\ntokens={tokens}")
|
||||
elif sub == "validate":
|
||||
ctx_path = project_root / "AirPlan" / "context.md"
|
||||
if ctx_path.exists():
|
||||
validation = validate_compression_with_fallback(project_root, ctx_path)
|
||||
ok = validation["level"] != CompressionLevel.TRUNCATE
|
||||
print(f"airplan_mode=ctx\nvalidation_ok={ok}\nlevel={validation['level']}\ntokens={validation['tokens']}")
|
||||
if "warning" in validation:
|
||||
print(f"warning={validation['warning']}")
|
||||
else:
|
||||
print("airplan_mode=ctx\nvalidation_ok=true")
|
||||
elif sub == "compress":
|
||||
# 读取 context 文件
|
||||
ctx_path = project_root / "AirPlan" / "context.md"
|
||||
if not ctx_path.exists():
|
||||
print("error: context.md not found")
|
||||
return
|
||||
|
||||
# 调用三级降级压缩
|
||||
content = ctx_path.read_text()
|
||||
result = compress_with_fallback(content)
|
||||
|
||||
log = EventLog(event_log_path(project_root))
|
||||
log.emit(CONTEXT_COMPACTED, {
|
||||
"compressionLevel": result["level"],
|
||||
"originalChars": len(content),
|
||||
"resultChars": len(result["result"]),
|
||||
"tokens": result.get("tokens", 0),
|
||||
})
|
||||
|
||||
print(f"airplan_mode=ctx")
|
||||
print(f"compression_level={result['level']}")
|
||||
print(f"original_chars={len(content)}")
|
||||
print(f"result_chars={len(result['result'])}")
|
||||
if 'warning' in result:
|
||||
print(f"warning={result['warning']}")
|
||||
|
||||
# 可选:写回压缩结果
|
||||
if getattr(args, "write_back", False):
|
||||
compressed_path = project_root / "AirPlan" / "context.compressed.md"
|
||||
compressed_path.write_text(result['result'])
|
||||
print(f"written_to={compressed_path}")
|
||||
else:
|
||||
state = safe_json_load(paths["state"]) or {}
|
||||
print(f"airplan_mode=ctx\nenabled={state.get('enabled', False)}")
|
||||
Reference in New Issue
Block a user