-
Notifications
You must be signed in to change notification settings - Fork 2
feat(agentscope): implement D5 safe compression and auto-trigger #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
954c91f
ca8b061
fb52b56
f00abe7
09d6a31
2cfbcbb
11647ca
b918558
f54cb85
dfcaf45
30a1fb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,6 +113,11 @@ def __init__( | |
| tool_gateway: IToolGateway, | ||
| plan_provider: IToolProvider | None = None, | ||
| max_tool_rounds: int = 10, | ||
| auto_compress: bool = False, | ||
| compress_trigger_ratio: float = 0.9, | ||
| compress_target_ratio: float = 0.75, | ||
| compress_max_messages: int | None = None, | ||
| compress_strategy: str = "dedup_then_truncate", | ||
| agent_channel: AgentChannel | None = None, | ||
| ) -> None: | ||
| super().__init__(name, agent_channel=agent_channel) | ||
|
|
@@ -121,6 +126,11 @@ def __init__( | |
| self._context = context | ||
| self._tool_gateway = tool_gateway | ||
| self._plan_provider = plan_provider | ||
| self._auto_compress = bool(auto_compress) | ||
| self._compress_trigger_ratio = _clamp_ratio(compress_trigger_ratio, default=0.9) | ||
| self._compress_target_ratio = _clamp_ratio(compress_target_ratio, default=0.75) | ||
| self._compress_max_messages = compress_max_messages if isinstance(compress_max_messages, int) and compress_max_messages > 0 else None | ||
| self._compress_strategy = compress_strategy.strip() if isinstance(compress_strategy, str) and compress_strategy.strip() else "dedup_then_truncate" | ||
| self._context.set_tool_gateway(self._tool_gateway) | ||
|
|
||
| # 运行时检测是否可以启用 SmartContext 能力 | ||
|
|
@@ -166,28 +176,10 @@ async def _execute_basic( | |
| for round_idx in range(self._max_tool_rounds): | ||
| print(f"[{self.name}] Round {round_idx + 1}/{self._max_tool_rounds}: 调用模型中...", flush=True) | ||
| assembled = self._context.assemble() | ||
| messages = list(assembled.messages) | ||
| prompt_def = getattr(assembled, "sys_prompt", None) | ||
| if prompt_def is not None: | ||
| messages = [ | ||
| Message( | ||
| role=prompt_def.role, | ||
| content=prompt_def.content, | ||
| name=prompt_def.name, | ||
| metadata=dict(prompt_def.metadata), | ||
| ), | ||
| *messages, | ||
| ] | ||
| # Inject critical_block from plan_provider (maintained by plan tools) | ||
| if self._plan_provider is not None: | ||
| state = getattr(self._plan_provider, "state", None) | ||
| critical_block = getattr(state, "critical_block", "") if state else "" | ||
| if critical_block: | ||
| print("\n--- [Plan State] (injected) ---\n" + critical_block + "\n---\n", flush=True) | ||
| messages.insert( | ||
| 1, | ||
| Message(role="system", content=critical_block, name="plan_state"), | ||
| ) | ||
| messages = self._build_model_messages(assembled) | ||
| if self._maybe_auto_compress(messages): | ||
| assembled = self._context.assemble() | ||
| messages = self._build_model_messages(assembled) | ||
|
|
||
| model_input = ModelInput( | ||
| messages=messages, | ||
|
|
@@ -403,10 +395,31 @@ async def _execute_with_smart_context( | |
| messages = self._context.order_messages_for_llm(messages, sys_prompt_message) | ||
|
|
||
| # 注入 _next_round_reflection_prompt:普通工具轮后提示,仅本次 LLM 调用传入,不写 STM | ||
| injected_reflection_prompt = self._next_round_reflection_prompt | ||
| if self._next_round_reflection_prompt is not None: | ||
| messages.append(self._next_round_reflection_prompt) | ||
| self._next_round_reflection_prompt = None | ||
|
|
||
| if self._maybe_auto_compress(messages): | ||
| assembled = self._context.assemble() | ||
| messages = list(assembled.messages) | ||
| prompt_def = getattr(assembled, "sys_prompt", None) | ||
| sys_prompt_message = ( | ||
| Message( | ||
| role=prompt_def.role, | ||
| content=prompt_def.content, | ||
| name=prompt_def.name, | ||
| metadata=dict(prompt_def.metadata), | ||
| mark=MessageMark.IMMUTABLE, | ||
| id="sys_prompt", | ||
| ) | ||
| if prompt_def is not None | ||
| else None | ||
| ) | ||
| messages = self._context.order_messages_for_llm(messages, sys_prompt_message) | ||
| if injected_reflection_prompt is not None: | ||
| messages.append(injected_reflection_prompt) | ||
|
|
||
| # Inject critical_block from plan_provider (maintained by plan tools) | ||
| # Disabled: skip injection to observe plan agent behavior without it | ||
| if False and self._plan_provider is not None: | ||
|
|
@@ -571,6 +584,62 @@ async def _execute_with_smart_context( | |
| output_text=final_message, | ||
| ) | ||
|
|
||
| def _build_model_messages(self, assembled: Any) -> list[Message]: | ||
| """Build model-facing messages including system prompt and plan state injection.""" | ||
| messages = list(assembled.messages) | ||
| prompt_def = getattr(assembled, "sys_prompt", None) | ||
| if prompt_def is not None: | ||
| messages = [ | ||
| Message( | ||
| role=prompt_def.role, | ||
| content=prompt_def.content, | ||
| name=prompt_def.name, | ||
| metadata=dict(prompt_def.metadata), | ||
| ), | ||
| *messages, | ||
| ] | ||
| if self._plan_provider is not None: | ||
| state = getattr(self._plan_provider, "state", None) | ||
| critical_block = getattr(state, "critical_block", "") if state else "" | ||
| if critical_block: | ||
| print("\n--- [Plan State] (injected) ---\n" + critical_block + "\n---\n", flush=True) | ||
| messages.insert( | ||
| 1, | ||
| Message(role="system", content=critical_block, name="plan_state"), | ||
| ) | ||
| return messages | ||
|
|
||
| def _maybe_auto_compress(self, model_messages: list[Message]) -> bool: | ||
| """Auto-compress context before model invocation when token estimate is near budget.""" | ||
| if not self._auto_compress: | ||
| return False | ||
| max_tokens = self._context.budget.max_tokens | ||
| if max_tokens is None or max_tokens <= 0: | ||
| return False | ||
|
|
||
| estimated_tokens = _estimate_messages_tokens(model_messages) | ||
| trigger_tokens = max(1, int(max_tokens * self._compress_trigger_ratio)) | ||
| if estimated_tokens < trigger_tokens: | ||
| return False | ||
|
|
||
| stm_messages = self._context.stm_get() | ||
| if not stm_messages: | ||
| return False | ||
| max_messages = self._compress_max_messages | ||
| if max_messages is None: | ||
| max_messages = max(1, int(len(stm_messages) * self._compress_target_ratio)) | ||
| if max_messages >= len(stm_messages): | ||
| max_messages = max(1, len(stm_messages) - 1) | ||
|
|
||
| target_tokens = max(1, int(max_tokens * self._compress_target_ratio)) | ||
| self._context.compress( | ||
| strategy=self._compress_strategy, | ||
| max_messages=max_messages, | ||
| target_tokens=target_tokens, | ||
| tool_pair_safe=True, | ||
| ) | ||
| return True | ||
|
|
||
| async def _emit_terminal_transport_message( | ||
| self, | ||
| *, | ||
|
|
@@ -701,4 +770,24 @@ def _tool_calls_signature(tool_calls: list[dict[str, Any]]) -> tuple[str, ...]: | |
| return tuple(signature) | ||
|
|
||
|
|
||
| def _estimate_messages_tokens(messages: list[Message]) -> int: | ||
| total = 0 | ||
| for message in messages: | ||
| content = (message.content or "").strip() | ||
| total += max(1, len(content) // 4) + 8 | ||
| return total | ||
|
|
||
|
|
||
| def _clamp_ratio(value: Any, *, default: float) -> float: | ||
| try: | ||
| ratio = float(value) | ||
| except (TypeError, ValueError): | ||
| return default | ||
| if ratio <= 0: | ||
| return default | ||
| if ratio > 1: | ||
| return 1.0 | ||
| return ratio | ||
|
Comment on lines
+789
to
+793
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| __all__ = ["ReactAgent"] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new auto-compression hook is only invoked in
_execute_basicat this call site, butexecute()routes allSmartContextinstances to_execute_with_smart_context, where model calls still happen without_maybe_auto_compress. As a result,ReactAgent(auto_compress=True)is silently ineffective for SmartContext agents, so long smart-context runs continue without the intended pre-model compression and can still hit token-budget pressure.Useful? React with 👍 / 👎.