mirror of https://github.com/aliasrobotics/cai.git
fix: fire auto-compact inside Runner.run, not just at outer-loop level
Previously CAI_SUPPORT_INTERVAL only checked at the end of each outer
while-loop iteration (once per user input). In agentic sessions the agent
makes many successive tool calls inside a single Runner.run invocation,
so the check would only fire after the runner returned — too late, or
never if the runner was still running.
Fix:
- Add ContextCompactedError exception class.
- Add a count-based trigger at the top of _auto_compact_if_needed (which
fires on EVERY LLM API call, inside both get_response and stream_response).
When assistant-message count >= CAI_SUPPORT_INTERVAL:
1. Set the compact model to CAI_SUPPORT_MODEL temporarily.
2. Summarise via _ai_summarize_history (awaited in-situ, no asyncio.run).
3. Store summary in COMPACTED_SUMMARIES so get_system_prompt picks it
up on the next turn without needing agent reload.
4. Clear message_history + reset CAI_CONTEXT_USAGE.
5. Raise ContextCompactedError to abort the current runner invocation.
- cli.py catches ContextCompactedError in both streaming and non-streaming
runner call sites:
- Sets _post_compact_input = _last_user_input so the task is replayed.
- Re-syncs the local agent reference via AGENT_MANAGER.
- Continues the outer while-loop, restarting with a clean context window.
The existing outer-loop CLI check (counting assistant messages in history
after the runner finishes) is kept as a belt-and-suspenders fallback.
This commit is contained in:
parent
85324782e4
commit
6ee2f0d66b
|
|
@ -313,6 +313,7 @@ from cai.sdk.agents.exceptions import OutputGuardrailTripwireTriggered, InputGua
|
|||
from cai.sdk.agents.models.openai_chatcompletions import (
|
||||
get_agent_message_history,
|
||||
get_all_agent_histories,
|
||||
ContextCompactedError,
|
||||
)
|
||||
# Import handled where needed to avoid circular imports
|
||||
from cai.sdk.agents.run_to_jsonl import get_session_recorder
|
||||
|
|
@ -1576,6 +1577,9 @@ def run_cai_cli(
|
|||
pass
|
||||
|
||||
raise e
|
||||
except ContextCompactedError:
|
||||
# Propagate so the outer try block can handle the restart.
|
||||
raise
|
||||
except Exception as e:
|
||||
# Clean up on any other exception
|
||||
if stream_iterator is not None:
|
||||
|
|
@ -1603,6 +1607,17 @@ def run_cai_cli(
|
|||
|
||||
try:
|
||||
asyncio.run(process_streamed_response(agent, conversation_input))
|
||||
except ContextCompactedError:
|
||||
# Auto-compact fired mid-runner; restart with fresh context.
|
||||
_post_compact_input = _last_user_input or "Continue the current task."
|
||||
from cai.sdk.agents.simple_agent_manager import AGENT_MANAGER as _AM
|
||||
_reloaded = _AM.get_active_agent()
|
||||
if _reloaded is not None:
|
||||
agent = _reloaded
|
||||
console.print(
|
||||
"[bold green]✓ Context window reset — resuming task[/bold green]\n"
|
||||
)
|
||||
continue
|
||||
except OutputGuardrailTripwireTriggered as e:
|
||||
# Display a user-friendly warning instead of crashing (streaming mode)
|
||||
guardrail_name = e.guardrail_result.guardrail.get_name()
|
||||
|
|
@ -1662,6 +1677,17 @@ def run_cai_cli(
|
|||
# Use non-streamed response
|
||||
try:
|
||||
response = asyncio.run(Runner.run(agent, conversation_input))
|
||||
except ContextCompactedError:
|
||||
# Auto-compact fired mid-runner; restart with fresh context.
|
||||
_post_compact_input = _last_user_input or "Continue the current task."
|
||||
from cai.sdk.agents.simple_agent_manager import AGENT_MANAGER as _AM
|
||||
_reloaded = _AM.get_active_agent()
|
||||
if _reloaded is not None:
|
||||
agent = _reloaded
|
||||
console.print(
|
||||
"[bold green]✓ Context window reset — resuming task[/bold green]\n"
|
||||
)
|
||||
continue
|
||||
except InputGuardrailTripwireTriggered as e:
|
||||
# Display a user-friendly warning for input guardrails
|
||||
reason = "Potential security threat detected in input"
|
||||
|
|
|
|||
|
|
@ -363,6 +363,13 @@ def count_tokens_with_tiktoken(text_or_messages):
|
|||
return 0, 0
|
||||
|
||||
|
||||
class ContextCompactedError(Exception):
|
||||
"""Raised inside get_response/stream_response when a CAI_SUPPORT_INTERVAL-based
|
||||
auto-compact fires mid-runner. The outer CLI loop catches this, sets
|
||||
_post_compact_input, and restarts the runner with a clean context window."""
|
||||
pass
|
||||
|
||||
|
||||
class OpenAIChatCompletionsModel(Model):
|
||||
"""OpenAI Chat Completions Model"""
|
||||
|
||||
|
|
@ -3475,7 +3482,67 @@ class OpenAIChatCompletionsModel(Model):
|
|||
# Check if auto-compaction is disabled
|
||||
if os.getenv("CAI_AUTO_COMPACT", "true").lower() == "false":
|
||||
return input, system_instructions, False
|
||||
|
||||
|
||||
# --- CAI_SUPPORT_INTERVAL count-based trigger ---
|
||||
# This fires on EVERY API call (not just at the outer CLI-loop level), so it correctly
|
||||
# handles agentic sessions where the agent makes many tool calls inside one Runner.run.
|
||||
_support_model = os.getenv("CAI_SUPPORT_MODEL")
|
||||
_support_interval_raw = os.getenv("CAI_SUPPORT_INTERVAL")
|
||||
if _support_model and _support_interval_raw:
|
||||
try:
|
||||
_support_interval = int(_support_interval_raw)
|
||||
if _support_interval > 0:
|
||||
_asst_count = sum(
|
||||
1 for m in self.message_history
|
||||
if isinstance(m, dict) and m.get("role") == "assistant"
|
||||
)
|
||||
if _asst_count >= _support_interval:
|
||||
from rich.console import Console as _Console
|
||||
_console = _Console()
|
||||
_console.print(
|
||||
f"\n[bold yellow]⟳ Auto-compact: {_asst_count} LLM responses "
|
||||
f"(threshold {_support_interval}) — summarising with "
|
||||
f"{_support_model}[/bold yellow]"
|
||||
)
|
||||
try:
|
||||
from cai.repl.commands.memory import (
|
||||
MEMORY_COMMAND_INSTANCE,
|
||||
COMPACTED_SUMMARIES,
|
||||
APPLIED_MEMORY_IDS,
|
||||
)
|
||||
from cai.repl.commands.compact import COMPACT_COMMAND_INSTANCE
|
||||
_orig_compact = COMPACT_COMMAND_INSTANCE.compact_model
|
||||
COMPACT_COMMAND_INSTANCE.compact_model = _support_model
|
||||
try:
|
||||
_summary = await MEMORY_COMMAND_INSTANCE._ai_summarize_history(
|
||||
self.agent_name
|
||||
)
|
||||
finally:
|
||||
COMPACT_COMMAND_INSTANCE.compact_model = _orig_compact
|
||||
if _summary:
|
||||
if self.agent_name not in COMPACTED_SUMMARIES:
|
||||
COMPACTED_SUMMARIES[self.agent_name] = []
|
||||
APPLIED_MEMORY_IDS[self.agent_name] = []
|
||||
COMPACTED_SUMMARIES[self.agent_name] = [_summary]
|
||||
self.message_history.clear()
|
||||
os.environ["CAI_CONTEXT_USAGE"] = "0.0"
|
||||
_console.print(
|
||||
"[bold green]✓ Memory summary applied — "
|
||||
"context window reset — restarting task[/bold green]\n"
|
||||
)
|
||||
except Exception as _ce:
|
||||
_console.print(f"[red]Auto-compact error: {_ce}[/red]")
|
||||
# Always abort the current runner invocation so the outer loop
|
||||
# can restart with our freshly cleared context.
|
||||
raise ContextCompactedError(
|
||||
f"Context compacted after {_asst_count} LLM responses "
|
||||
f"(threshold {_support_interval})"
|
||||
)
|
||||
except ContextCompactedError:
|
||||
raise # propagate to the outer runner / CLI loop
|
||||
except (ValueError, Exception):
|
||||
pass # malformed interval — ignore silently
|
||||
|
||||
max_tokens = self._get_model_max_tokens(str(self.model))
|
||||
threshold_percent = float(os.getenv("CAI_AUTO_COMPACT_THRESHOLD", "0.8"))
|
||||
threshold = max_tokens * threshold_percent
|
||||
|
|
|
|||
Loading…
Reference in New Issue