|
|
@@ -35,9 +35,12 @@ from agent.failures import FailureDetail, FailureDisposition
|
|
|
from agent.trace.protocols import TraceAttachmentStore, TraceStore
|
|
|
from agent.trace.goal_models import GoalTree
|
|
|
from agent.trace.compaction import (
|
|
|
+ PromptTokenEstimate,
|
|
|
CompressionConfig,
|
|
|
+ calibrate_prompt_estimate,
|
|
|
compress_completed_goals,
|
|
|
estimate_tokens,
|
|
|
+ measure_prompt_tokens,
|
|
|
)
|
|
|
from agent.skill.models import Skill
|
|
|
from agent.tools import ToolRegistry, get_tool_registry
|
|
|
@@ -93,6 +96,14 @@ class ContextUsage:
|
|
|
max_tokens: int
|
|
|
usage_percent: float
|
|
|
image_count: int = 0
|
|
|
+ estimated_prompt_tokens: int = 0
|
|
|
+ calibrated_prompt_tokens: int = 0
|
|
|
+ actual_prompt_tokens: Optional[int] = None
|
|
|
+ tool_schema_tokens: int = 0
|
|
|
+ calibration_factor: float = 1.0
|
|
|
+ compression_count: int = 0
|
|
|
+ trigger_tokens: int = 0
|
|
|
+ target_tokens: int = 0
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
@@ -173,6 +184,7 @@ class RunConfig:
|
|
|
None # explicit Planner 新 Trace 的 Root Task 草案
|
|
|
)
|
|
|
no_progress: NoProgressPolicy = field(default_factory=NoProgressPolicy)
|
|
|
+ compression: CompressionConfig = field(default_factory=CompressionConfig)
|
|
|
|
|
|
# --- Trace 控制 ---
|
|
|
trace_id: Optional[str] = None # None = 新建
|
|
|
@@ -872,18 +884,23 @@ class AgentRunner:
|
|
|
config: RunConfig,
|
|
|
sequence: int,
|
|
|
head_seq: int,
|
|
|
+ tool_schemas: Optional[List[Dict[str, Any]]] = None,
|
|
|
) -> Tuple[List[Dict], int, int, bool]:
|
|
|
- """
|
|
|
- 管理 context 用量:检查、预警、压缩
|
|
|
-
|
|
|
- Returns:
|
|
|
- (updated_history, new_head_seq, next_sequence, needs_enter_compression_branch)
|
|
|
- """
|
|
|
- compression_config = CompressionConfig()
|
|
|
- token_count = estimate_tokens(history)
|
|
|
- max_tokens = compression_config.get_max_tokens(config.model)
|
|
|
+ """Measure calibrated prompt size and start compression before overflow."""
|
|
|
|
|
|
- # 计算使用率
|
|
|
+ trace = await self.trace_store.get_trace(trace_id) if self.trace_store else None
|
|
|
+ budget_state = (
|
|
|
+ trace.runtime_state.setdefault("context_budget", {}) if trace else {}
|
|
|
+ )
|
|
|
+ measurement = measure_prompt_tokens(
|
|
|
+ history,
|
|
|
+ tool_schemas or [],
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ budget_state.get("calibration_factor"),
|
|
|
+ )
|
|
|
+ token_count = measurement.calibrated_tokens
|
|
|
+ max_tokens = measurement.hard_limit
|
|
|
progress_pct = (token_count / max_tokens * 100) if max_tokens > 0 else 0
|
|
|
msg_count = len(history)
|
|
|
img_count = sum(
|
|
|
@@ -902,13 +919,20 @@ class AgentRunner:
|
|
|
max_tokens=max_tokens,
|
|
|
usage_percent=progress_pct,
|
|
|
image_count=img_count,
|
|
|
+ estimated_prompt_tokens=measurement.estimated_tokens,
|
|
|
+ calibrated_prompt_tokens=measurement.calibrated_tokens,
|
|
|
+ actual_prompt_tokens=budget_state.get("last_actual_prompt_tokens"),
|
|
|
+ tool_schema_tokens=measurement.tool_schema_tokens,
|
|
|
+ calibration_factor=measurement.calibration_factor,
|
|
|
+ compression_count=int(budget_state.get("compression_count", 0)),
|
|
|
+ trigger_tokens=measurement.trigger_tokens,
|
|
|
+ target_tokens=measurement.target_tokens,
|
|
|
)
|
|
|
|
|
|
- # 阈值警告(30%, 50%, 80%)
|
|
|
if trace_id not in self._context_warned:
|
|
|
self._context_warned[trace_id] = set()
|
|
|
|
|
|
- for threshold in [30, 50, 80]:
|
|
|
+ for threshold in [30, 50, 75]:
|
|
|
if (
|
|
|
progress_pct >= threshold
|
|
|
and threshold not in self._context_warned[trace_id]
|
|
|
@@ -918,47 +942,38 @@ class AgentRunner:
|
|
|
f"Context 使用率达到 {threshold}%: {token_count:,} / {max_tokens:,} tokens ({msg_count} 条消息)"
|
|
|
)
|
|
|
|
|
|
- # 检查是否需要压缩(仅基于 token 数量)
|
|
|
- needs_compression = token_count > max_tokens
|
|
|
+ needs_compression = (
|
|
|
+ token_count >= measurement.trigger_tokens
|
|
|
+ or (
|
|
|
+ config.compression.max_messages > 0
|
|
|
+ and msg_count >= config.compression.max_messages
|
|
|
+ )
|
|
|
+ )
|
|
|
|
|
|
if not needs_compression:
|
|
|
return history, head_seq, sequence, False
|
|
|
|
|
|
- # 检查是否有待评估知识(压缩前必须先评估)
|
|
|
- if self.trace_store and not config.force_side_branch:
|
|
|
- pending = await self.trace_store.get_pending_knowledge_entries(trace_id)
|
|
|
- if pending:
|
|
|
- # 设置侧分支队列:反思 → 知识评估 → 压缩
|
|
|
- # 反思放在前面,确保反思期间完成的 goal 产生的新知识也能在压缩前被评估
|
|
|
- if config.knowledge.enable_extraction:
|
|
|
- config.force_side_branch = [
|
|
|
- "reflection",
|
|
|
- "knowledge_eval",
|
|
|
- "compression",
|
|
|
- ]
|
|
|
- else:
|
|
|
- config.force_side_branch = ["knowledge_eval", "compression"]
|
|
|
-
|
|
|
- # 在 trace.context 中设置触发事件
|
|
|
- trace = await self.trace_store.get_trace(trace_id)
|
|
|
- if trace:
|
|
|
- if not trace.context:
|
|
|
- trace.context = {}
|
|
|
- trace.context["knowledge_eval_trigger"] = "compression"
|
|
|
- await self.trace_store.update_trace(trace_id, context=trace.context)
|
|
|
-
|
|
|
- self.log.info(
|
|
|
- f"[Knowledge Eval] 压缩前触发知识评估,待评估: {len(pending)} 条"
|
|
|
- )
|
|
|
- return history, head_seq, sequence, True
|
|
|
-
|
|
|
- # 知识提取:在任何压缩发生前,用完整 history 做反思(进入反思侧分支)
|
|
|
- if config.knowledge.enable_extraction and not config.force_side_branch:
|
|
|
- # 设置侧分支队列:先反思,再压缩
|
|
|
- config.force_side_branch = ["reflection", "compression"]
|
|
|
- return history, head_seq, sequence, True
|
|
|
+ if trace and not budget_state.get("compression_pending"):
|
|
|
+ budget_state["compression_pending"] = True
|
|
|
+ budget_state["compression_started_at"] = datetime.now().isoformat()
|
|
|
+ started_payload = self._context_event_payload(
|
|
|
+ measurement,
|
|
|
+ message_count=msg_count,
|
|
|
+ actual_prompt_tokens=budget_state.get("last_actual_prompt_tokens"),
|
|
|
+ reason="trigger_ratio" if token_count >= measurement.trigger_tokens else "message_limit",
|
|
|
+ )
|
|
|
+ budget_state["compression_before"] = started_payload
|
|
|
+ trace.runtime_state["context_budget"] = budget_state
|
|
|
+ await self.trace_store.update_trace(
|
|
|
+ trace_id,
|
|
|
+ runtime_state=trace.runtime_state,
|
|
|
+ )
|
|
|
+ await self.trace_store.append_event(
|
|
|
+ trace_id,
|
|
|
+ "context_compression_started",
|
|
|
+ started_payload,
|
|
|
+ )
|
|
|
|
|
|
- # Level 1 是 legacy GoalTree 的确定性压缩;explicit 直接保留 Level 2。
|
|
|
is_legacy = (
|
|
|
CompletionPolicy(config.completion_policy) == CompletionPolicy.LEGACY_AUTO
|
|
|
)
|
|
|
@@ -991,41 +1006,192 @@ class AgentRunner:
|
|
|
token_count,
|
|
|
)
|
|
|
|
|
|
- # Level 2 压缩:检查 Level 1 后是否仍超阈值
|
|
|
- # 注意:Level 1 压缩后需要重新优化图片并计算 token
|
|
|
optimized_history_after = await self._optimize_images(history, config.model)
|
|
|
- token_count_after = estimate_tokens(optimized_history_after)
|
|
|
- needs_level2 = token_count_after > max_tokens
|
|
|
+ after = measure_prompt_tokens(
|
|
|
+ optimized_history_after,
|
|
|
+ tool_schemas or [],
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ measurement.calibration_factor,
|
|
|
+ )
|
|
|
+ needs_level2 = after.calibrated_tokens > after.target_tokens
|
|
|
|
|
|
if needs_level2:
|
|
|
source = "Level 1 后" if is_legacy else "explicit 模式"
|
|
|
self.log.info(
|
|
|
"%s仍超阈值 (token=%d/%d),需要进入压缩侧分支",
|
|
|
source,
|
|
|
- token_count_after,
|
|
|
- max_tokens,
|
|
|
+ after.calibrated_tokens,
|
|
|
+ after.hard_limit,
|
|
|
)
|
|
|
- # 如果还没有设置侧分支(说明没有启用知识提取),直接进入压缩
|
|
|
if not config.force_side_branch:
|
|
|
config.force_side_branch = ["compression"]
|
|
|
- # 返回标志,让主循环进入侧分支
|
|
|
return history, head_seq, sequence, True
|
|
|
|
|
|
- # 压缩完成后,输出最终发给模型的消息列表
|
|
|
- self.log.info("上下文压缩检查完成,发送给模型的消息列表:")
|
|
|
- for idx, msg in enumerate(history):
|
|
|
- role = msg.get("role", "unknown")
|
|
|
- content = msg.get("content", "")
|
|
|
- if isinstance(content, str):
|
|
|
- preview = content[:100] + ("..." if len(content) > 100 else "")
|
|
|
- elif isinstance(content, list):
|
|
|
- preview = f"[{len(content)} blocks]"
|
|
|
- else:
|
|
|
- preview = str(content)[:100]
|
|
|
- self.log.info(f" [{idx}] {role}: {preview}")
|
|
|
-
|
|
|
+ await self._finish_context_compression(
|
|
|
+ trace,
|
|
|
+ after,
|
|
|
+ before_message_count=msg_count,
|
|
|
+ after_message_count=len(history),
|
|
|
+ )
|
|
|
return history, head_seq, sequence, False
|
|
|
|
|
|
+ @staticmethod
|
|
|
+ def _context_event_payload(
|
|
|
+ measurement: PromptTokenEstimate,
|
|
|
+ *,
|
|
|
+ message_count: int,
|
|
|
+ actual_prompt_tokens: Optional[int],
|
|
|
+ reason: str,
|
|
|
+ ) -> Dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "message_count": message_count,
|
|
|
+ "estimated_prompt_tokens": measurement.estimated_tokens,
|
|
|
+ "calibrated_prompt_tokens": measurement.calibrated_tokens,
|
|
|
+ "actual_prompt_tokens": actual_prompt_tokens,
|
|
|
+ "tool_schema_tokens": measurement.tool_schema_tokens,
|
|
|
+ "calibration_factor": measurement.calibration_factor,
|
|
|
+ "trigger_tokens": measurement.trigger_tokens,
|
|
|
+ "target_tokens": measurement.target_tokens,
|
|
|
+ "hard_limit": measurement.hard_limit,
|
|
|
+ "reason": reason,
|
|
|
+ }
|
|
|
+
|
|
|
+ async def _record_prompt_measurement(
|
|
|
+ self,
|
|
|
+ trace: Trace,
|
|
|
+ config: RunConfig,
|
|
|
+ messages: List[Dict[str, Any]],
|
|
|
+ tool_schemas: List[Dict[str, Any]],
|
|
|
+ actual_prompt_tokens: int,
|
|
|
+ ) -> None:
|
|
|
+ budget = trace.runtime_state.setdefault("context_budget", {})
|
|
|
+ raw = measure_prompt_tokens(
|
|
|
+ messages,
|
|
|
+ tool_schemas,
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ 1.0,
|
|
|
+ )
|
|
|
+ previous = float(
|
|
|
+ budget.get(
|
|
|
+ "calibration_factor",
|
|
|
+ config.compression.fallback_safety_factor,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ budget["calibration_factor"] = calibrate_prompt_estimate(
|
|
|
+ actual_prompt_tokens=actual_prompt_tokens,
|
|
|
+ estimated_prompt_tokens=raw.estimated_tokens,
|
|
|
+ previous_factor=previous,
|
|
|
+ )
|
|
|
+ budget["last_actual_prompt_tokens"] = actual_prompt_tokens
|
|
|
+ budget["last_estimated_prompt_tokens"] = raw.estimated_tokens
|
|
|
+ await self._persist_runtime_state(trace)
|
|
|
+
|
|
|
+ async def _finish_context_compression(
|
|
|
+ self,
|
|
|
+ trace: Optional[Trace],
|
|
|
+ measurement: PromptTokenEstimate,
|
|
|
+ *,
|
|
|
+ before_message_count: int,
|
|
|
+ after_message_count: int,
|
|
|
+ ) -> Optional[FailureDetail]:
|
|
|
+ if trace is None or self.trace_store is None:
|
|
|
+ return None
|
|
|
+ budget = trace.runtime_state.setdefault("context_budget", {})
|
|
|
+ started_at = budget.get("compression_started_at")
|
|
|
+ duration_ms = None
|
|
|
+ if isinstance(started_at, str):
|
|
|
+ try:
|
|
|
+ duration_ms = max(
|
|
|
+ 0,
|
|
|
+ int((datetime.now() - datetime.fromisoformat(started_at)).total_seconds() * 1000),
|
|
|
+ )
|
|
|
+ except ValueError:
|
|
|
+ duration_ms = None
|
|
|
+ budget["compression_pending"] = False
|
|
|
+ budget.pop("compression_started_at", None)
|
|
|
+ before_measurement = budget.pop("compression_before", None)
|
|
|
+ budget["compression_count"] = int(budget.get("compression_count", 0)) + 1
|
|
|
+ await self._persist_runtime_state(trace)
|
|
|
+ payload = {
|
|
|
+ **self._context_event_payload(
|
|
|
+ measurement,
|
|
|
+ message_count=after_message_count,
|
|
|
+ actual_prompt_tokens=budget.get("last_actual_prompt_tokens"),
|
|
|
+ reason="compression_completed",
|
|
|
+ ),
|
|
|
+ "before_message_count": before_message_count,
|
|
|
+ "after_message_count": after_message_count,
|
|
|
+ "duration_ms": duration_ms,
|
|
|
+ "compression_count": budget["compression_count"],
|
|
|
+ "before": before_measurement,
|
|
|
+ }
|
|
|
+ usage = self._context_usage.get(trace.trace_id)
|
|
|
+ if usage is not None:
|
|
|
+ usage.message_count = after_message_count
|
|
|
+ usage.token_count = measurement.calibrated_tokens
|
|
|
+ usage.estimated_prompt_tokens = measurement.estimated_tokens
|
|
|
+ usage.calibrated_prompt_tokens = measurement.calibrated_tokens
|
|
|
+ usage.tool_schema_tokens = measurement.tool_schema_tokens
|
|
|
+ usage.calibration_factor = measurement.calibration_factor
|
|
|
+ usage.compression_count = budget["compression_count"]
|
|
|
+ usage.usage_percent = (
|
|
|
+ measurement.calibrated_tokens / measurement.hard_limit * 100
|
|
|
+ if measurement.hard_limit
|
|
|
+ else 0
|
|
|
+ )
|
|
|
+ if measurement.calibrated_tokens > measurement.target_tokens:
|
|
|
+ return await self._context_budget_exceeded(
|
|
|
+ trace,
|
|
|
+ measurement,
|
|
|
+ reason="compression_target_not_reached",
|
|
|
+ extra=payload,
|
|
|
+ )
|
|
|
+ await self.trace_store.append_event(
|
|
|
+ trace.trace_id,
|
|
|
+ "context_compression_completed",
|
|
|
+ payload,
|
|
|
+ )
|
|
|
+ return None
|
|
|
+
|
|
|
+ async def _context_budget_exceeded(
|
|
|
+ self,
|
|
|
+ trace: Trace,
|
|
|
+ measurement: PromptTokenEstimate,
|
|
|
+ *,
|
|
|
+ reason: str,
|
|
|
+ extra: Optional[Dict[str, Any]] = None,
|
|
|
+ ) -> FailureDetail:
|
|
|
+ payload = {
|
|
|
+ **self._context_event_payload(
|
|
|
+ measurement,
|
|
|
+ message_count=int((extra or {}).get("after_message_count", 0)),
|
|
|
+ actual_prompt_tokens=trace.runtime_state.get("context_budget", {}).get(
|
|
|
+ "last_actual_prompt_tokens"
|
|
|
+ ),
|
|
|
+ reason=reason,
|
|
|
+ ),
|
|
|
+ **(extra or {}),
|
|
|
+ }
|
|
|
+ if self.trace_store:
|
|
|
+ await self.trace_store.append_event(
|
|
|
+ trace.trace_id,
|
|
|
+ "context_budget_exceeded",
|
|
|
+ payload,
|
|
|
+ )
|
|
|
+ return FailureDetail(
|
|
|
+ code="CONTEXT_BUDGET_EXCEEDED",
|
|
|
+ message="The minimum safe execution context exceeds the configured token budget",
|
|
|
+ disposition=FailureDisposition.ABORT_RUN,
|
|
|
+ details={
|
|
|
+ "reason": reason,
|
|
|
+ "calibrated_prompt_tokens": measurement.calibrated_tokens,
|
|
|
+ "hard_limit": measurement.hard_limit,
|
|
|
+ "target_tokens": measurement.target_tokens,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
async def _build_knowledge_eval_prompt(
|
|
|
self, trace_id: str, goal_tree: Optional[GoalTree]
|
|
|
) -> str:
|
|
|
@@ -1115,7 +1281,13 @@ class AgentRunner:
|
|
|
goal_tree,
|
|
|
config,
|
|
|
)
|
|
|
- compress_prompt = build_single_turn_prompt(execution_context)
|
|
|
+ compress_prompt = build_single_turn_prompt(
|
|
|
+ execution_context,
|
|
|
+ explicit=(
|
|
|
+ CompletionPolicy(config.completion_policy)
|
|
|
+ == CompletionPolicy.EXPLICIT_VALIDATION
|
|
|
+ ),
|
|
|
+ )
|
|
|
compress_messages = list(history) + [
|
|
|
{"role": "user", "content": compress_prompt}
|
|
|
]
|
|
|
@@ -1124,6 +1296,27 @@ class AgentRunner:
|
|
|
compress_messages = self._add_cache_control(
|
|
|
compress_messages, config.model, config.enable_prompt_caching
|
|
|
)
|
|
|
+ trace = await self.trace_store.get_trace(trace_id) if self.trace_store else None
|
|
|
+ measurement = measure_prompt_tokens(
|
|
|
+ compress_messages,
|
|
|
+ [],
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ (
|
|
|
+ trace.runtime_state.get("context_budget", {}).get("calibration_factor")
|
|
|
+ if trace
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ if measurement.calibrated_tokens > measurement.hard_limit:
|
|
|
+ if trace:
|
|
|
+ await self._context_budget_exceeded(
|
|
|
+ trace,
|
|
|
+ measurement,
|
|
|
+ reason="compression_fallback_would_exceed_hard_limit",
|
|
|
+ extra={"after_message_count": len(compress_messages)},
|
|
|
+ )
|
|
|
+ return ""
|
|
|
|
|
|
# 单次 LLM 调用(无工具)
|
|
|
result = await self.llm_call(
|
|
|
@@ -1133,6 +1326,14 @@ class AgentRunner:
|
|
|
temperature=config.temperature,
|
|
|
**config.extra_llm_params,
|
|
|
)
|
|
|
+ if trace and int(result.get("prompt_tokens", 0)) > 0:
|
|
|
+ await self._record_prompt_measurement(
|
|
|
+ trace,
|
|
|
+ config,
|
|
|
+ compress_messages,
|
|
|
+ [],
|
|
|
+ int(result["prompt_tokens"]),
|
|
|
+ )
|
|
|
|
|
|
summary_text = result.get("content", "").strip()
|
|
|
|
|
|
@@ -1362,7 +1563,13 @@ class AgentRunner:
|
|
|
sequence,
|
|
|
needs_enter_side_branch,
|
|
|
) = await self._manage_context_usage(
|
|
|
- trace_id, history, goal_tree, config, sequence, head_seq
|
|
|
+ trace_id,
|
|
|
+ history,
|
|
|
+ goal_tree,
|
|
|
+ config,
|
|
|
+ sequence,
|
|
|
+ head_seq,
|
|
|
+ tool_schemas,
|
|
|
)
|
|
|
|
|
|
# 进入侧分支
|
|
|
@@ -1450,7 +1657,10 @@ class AgentRunner:
|
|
|
goal_tree,
|
|
|
config,
|
|
|
)
|
|
|
- prompt = build_compression_prompt(execution_context)
|
|
|
+ prompt = build_compression_prompt(
|
|
|
+ execution_context,
|
|
|
+ explicit=(explicit_role is not None),
|
|
|
+ )
|
|
|
|
|
|
branch_user_msg = Message.create(
|
|
|
trace_id=trace_id,
|
|
|
@@ -1487,6 +1697,23 @@ class AgentRunner:
|
|
|
llm_messages, config.model, config.enable_prompt_caching
|
|
|
)
|
|
|
|
|
|
+ budget_state = trace.runtime_state.get("context_budget", {})
|
|
|
+ request_measurement = measure_prompt_tokens(
|
|
|
+ llm_messages,
|
|
|
+ tool_schemas,
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ budget_state.get("calibration_factor"),
|
|
|
+ )
|
|
|
+ if request_measurement.calibrated_tokens > request_measurement.hard_limit:
|
|
|
+ terminal_failure = await self._context_budget_exceeded(
|
|
|
+ trace,
|
|
|
+ request_measurement,
|
|
|
+ reason="request_would_exceed_hard_limit",
|
|
|
+ extra={"after_message_count": len(llm_messages)},
|
|
|
+ )
|
|
|
+ break
|
|
|
+
|
|
|
# 调用 LLM(等待完成后再检查 cancel 信号,不中断正在进行的调用)
|
|
|
result = await self.llm_call(
|
|
|
messages=llm_messages,
|
|
|
@@ -1505,6 +1732,14 @@ class AgentRunner:
|
|
|
step_cost = result.get("cost", 0)
|
|
|
cache_creation_tokens = result.get("cache_creation_tokens")
|
|
|
cache_read_tokens = result.get("cache_read_tokens")
|
|
|
+ if prompt_tokens > 0:
|
|
|
+ await self._record_prompt_measurement(
|
|
|
+ trace,
|
|
|
+ config,
|
|
|
+ llm_messages,
|
|
|
+ tool_schemas,
|
|
|
+ prompt_tokens,
|
|
|
+ )
|
|
|
|
|
|
# 周期性自动注入上下文(仅主路径)
|
|
|
if (
|
|
|
@@ -1821,6 +2056,22 @@ class AgentRunner:
|
|
|
history = history[: side_branch_ctx.start_history_length]
|
|
|
head_seq = side_branch_ctx.start_head_seq
|
|
|
|
|
|
+ compacted_measurement = measure_prompt_tokens(
|
|
|
+ history,
|
|
|
+ tool_schemas,
|
|
|
+ config.compression,
|
|
|
+ config.model,
|
|
|
+ trace.runtime_state.get("context_budget", {}).get(
|
|
|
+ "calibration_factor"
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ compression_failure = await self._finish_context_compression(
|
|
|
+ trace,
|
|
|
+ compacted_measurement,
|
|
|
+ before_message_count=side_branch_ctx.start_history_length,
|
|
|
+ after_message_count=len(history),
|
|
|
+ )
|
|
|
+
|
|
|
# 清理
|
|
|
trace.context.pop("active_side_branch", None)
|
|
|
config.force_side_branch = None
|
|
|
@@ -1831,6 +2082,9 @@ class AgentRunner:
|
|
|
head_sequence=head_seq,
|
|
|
)
|
|
|
side_branch_ctx = None
|
|
|
+ if compression_failure is not None:
|
|
|
+ terminal_failure = compression_failure
|
|
|
+ break
|
|
|
continue
|
|
|
|
|
|
elif should_exit and side_branch_ctx.type == "reflection":
|