diff --git a/src/cai/cli.py b/src/cai/cli.py index c1694c7c..fde1c5be 100644 --- a/src/cai/cli.py +++ b/src/cai/cli.py @@ -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: diff --git a/src/cai/repl/commands/memory.py b/src/cai/repl/commands/memory.py index f2e283c8..0e1af02b 100644 --- a/src/cai/repl/commands/memory.py +++ b/src/cai/repl/commands/memory.py @@ -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."""