mirror of https://github.com/aliasrobotics/cai.git
fix: prevent post-compaction repeat work
Three root causes identified and fixed: 1. _format_history_for_summary dropped ALL tool outputs >500 chars with '[Long output truncated]', meaning nmap/gobuster/curl results — the exact evidence the summary model needs to write the 'Exhausted Approaches' section — were silently discarded. Increased limit to 2000 chars (first chars of each output), bumped the message cap from 50 to 200 blocks, and fixed the assistant tool-call extractor which only handled object-style tool_calls (not the dict-style format used in message_history), causing every command ever run to disappear. 2. _post_compact_input was set to the raw original user task (e.g. 'hack the box machine Cap'). That becomes the last message the LLM reads, overriding the memory acknowledgement and making the agent treat it as a brand-new task. Now injects an explicit anti-repetition instruction alongside the original task text. 3. (Previous fix) Summary prompt now includes §9 Exhausted Approaches and §10 Recommended Next Steps — this only works if the summary model actually sees the scan/tool data, which fix #1 now guarantees.
This commit is contained in:
parent
476efb3994
commit
6546d8cbde
|
|
@ -1613,7 +1613,16 @@ def run_cai_cli(
|
|||
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."
|
||||
_base = _last_user_input or "Continue the current task."
|
||||
_post_compact_input = (
|
||||
f"{_base}\n\n"
|
||||
"IMPORTANT: Your context window was just compacted. "
|
||||
"Your session memory is already loaded above. "
|
||||
"Review the 'Exhausted Approaches' section in your memory and "
|
||||
"DO NOT repeat any technique, command, URL, port scan, or login "
|
||||
"attempt already listed there. "
|
||||
"Pick up exactly where you left off using only NEW approaches."
|
||||
)
|
||||
from cai.sdk.agents.simple_agent_manager import AGENT_MANAGER as _AM
|
||||
_reloaded = _AM.get_active_agent()
|
||||
if _reloaded is not None:
|
||||
|
|
@ -1683,7 +1692,16 @@ def run_cai_cli(
|
|||
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."
|
||||
_base = _last_user_input or "Continue the current task."
|
||||
_post_compact_input = (
|
||||
f"{_base}\n\n"
|
||||
"IMPORTANT: Your context window was just compacted. "
|
||||
"Your session memory is already loaded above. "
|
||||
"Review the 'Exhausted Approaches' section in your memory and "
|
||||
"DO NOT repeat any technique, command, URL, port scan, or login "
|
||||
"attempt already listed there. "
|
||||
"Pick up exactly where you left off using only NEW approaches."
|
||||
)
|
||||
from cai.sdk.agents.simple_agent_manager import AGENT_MANAGER as _AM
|
||||
_reloaded = _AM.get_active_agent()
|
||||
if _reloaded is not None:
|
||||
|
|
|
|||
|
|
@ -1267,39 +1267,60 @@ This session is being continued from a previous conversation that ran out of con
|
|||
return None
|
||||
|
||||
def _format_history_for_summary(self, history: List[Dict[str, Any]]) -> str:
|
||||
"""Format message history for summarization."""
|
||||
"""Format message history for summarization.
|
||||
|
||||
Critical design goals:
|
||||
- Include EVERY tool call with its exact arguments (commands run, URLs visited,
|
||||
ports scanned) so the summary model can produce an "Exhausted Approaches" list.
|
||||
- Include enough of each tool result to convey success/failure and key findings.
|
||||
- Avoid blowing out the summary model's context by capping large outputs.
|
||||
"""
|
||||
TOOL_OUTPUT_KEEP = 2000 # chars to preserve from each tool result
|
||||
MAX_PARTS = 200 # maximum formatted blocks to pass (covers ~100 turns)
|
||||
|
||||
formatted_parts = []
|
||||
|
||||
|
||||
for msg in history:
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
|
||||
# Skip empty messages
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Format based on role
|
||||
|
||||
if role == "user":
|
||||
formatted_parts.append(f"USER: {content}")
|
||||
if content:
|
||||
formatted_parts.append(f"USER: {content}")
|
||||
|
||||
elif role == "assistant":
|
||||
# Check for tool calls
|
||||
if "tool_calls" in msg and msg["tool_calls"]:
|
||||
# -- tool calls: extract args from both dict-style and object-style entries --
|
||||
tool_calls = msg.get("tool_calls") or []
|
||||
if tool_calls:
|
||||
tool_info = []
|
||||
for tc in msg["tool_calls"]:
|
||||
if hasattr(tc, "function"):
|
||||
tool_info.append(f"{tc.function.name}({tc.function.arguments})")
|
||||
for tc in tool_calls:
|
||||
if isinstance(tc, dict):
|
||||
fn = tc.get("function", {})
|
||||
name = fn.get("name", "?")
|
||||
args = fn.get("arguments", "")
|
||||
tool_info.append(f"{name}({args})")
|
||||
elif hasattr(tc, "function"):
|
||||
tool_info.append(
|
||||
f"{tc.function.name}({tc.function.arguments})"
|
||||
)
|
||||
if tool_info:
|
||||
formatted_parts.append(f"ASSISTANT (tools): {', '.join(tool_info)}")
|
||||
formatted_parts.append(
|
||||
f"ASSISTANT called tools: {', '.join(tool_info)}"
|
||||
)
|
||||
if content:
|
||||
formatted_parts.append(f"ASSISTANT: {content}")
|
||||
|
||||
elif role == "tool":
|
||||
# Include important tool outputs
|
||||
if len(str(content)) < 500: # Only include short outputs
|
||||
formatted_parts.append(f"TOOL OUTPUT: {content}")
|
||||
raw = str(content) if content else ""
|
||||
if len(raw) <= TOOL_OUTPUT_KEEP:
|
||||
formatted_parts.append(f"TOOL OUTPUT:\n{raw}")
|
||||
else:
|
||||
formatted_parts.append(f"TOOL OUTPUT: [Long output truncated]")
|
||||
|
||||
return "\n\n".join(formatted_parts[-50:]) # Limit to last 50 exchanges
|
||||
head = raw[:TOOL_OUTPUT_KEEP]
|
||||
formatted_parts.append(
|
||||
f"TOOL OUTPUT (truncated to {TOOL_OUTPUT_KEEP} chars):\n{head}\n[...truncated]"
|
||||
)
|
||||
|
||||
return "\n\n".join(formatted_parts[-MAX_PARTS:])
|
||||
|
||||
def _get_current_agent_name(self) -> Optional[str]:
|
||||
"""Get the name of the current active agent."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue