Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions codeflash/cli_cmds/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,18 @@ def parse_args() -> Namespace:
"--reset-config", action="store_true", help="Remove codeflash configuration from project config file."
)
parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts (useful for CI/scripts).")
parser.add_argument(
"--subagent",
action="store_true",
help="Subagent mode: skip all interactive prompts with sensible defaults. Designed for AI agent integrations.",
)

args, unknown_args = parser.parse_known_args()
sys.argv[:] = [sys.argv[0], *unknown_args]
if args.subagent:
args.yes = True
args.no_pr = True
args.worktree = True
return process_and_validate_cmd_args(args)


Expand Down
162 changes: 146 additions & 16 deletions codeflash/cli_cmds/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from codeflash.cli_cmds.console_constants import SPINNER_TYPES
from codeflash.cli_cmds.logging_config import BARE_LOGGING_FORMAT
from codeflash.lsp.helpers import is_LSP_enabled
from codeflash.lsp.helpers import is_LSP_enabled, is_subagent_mode
from codeflash.lsp.lsp_logger import enhanced_log
from codeflash.lsp.lsp_message import LspCodeMessage, LspTextMessage

Expand All @@ -34,33 +34,60 @@
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
from codeflash.languages.base import DependencyResolver, IndexResult
from codeflash.lsp.lsp_message import LspMessage
from codeflash.models.models import TestResults

DEBUG_MODE = logging.getLogger().getEffectiveLevel() == logging.DEBUG

console = Console()

if is_LSP_enabled():
if is_LSP_enabled() or is_subagent_mode():
console.quiet = True

logging.basicConfig(
level=logging.INFO,
handlers=[RichHandler(rich_tracebacks=True, markup=False, console=console, show_path=False, show_time=False)],
format=BARE_LOGGING_FORMAT,
)
if is_subagent_mode():
import re
import sys

_lsp_prefix_re = re.compile(r"^(?:!?lsp,?|h[2-4]|loading)\|")
_subagent_drop_patterns = (
"Test log -",
"Test failed to load",
"Examining file ",
"Generated ",
"Add custom marker",
"Disabling all autouse",
"Reverting code and helpers",
)

class _AgentLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.msg = _lsp_prefix_re.sub("", str(record.msg))
msg = record.getMessage()
return not any(msg.startswith(p) for p in _subagent_drop_patterns)

_agent_handler = logging.StreamHandler(sys.stderr)
_agent_handler.addFilter(_AgentLogFilter())
logging.basicConfig(level=logging.INFO, handlers=[_agent_handler], format="%(levelname)s: %(message)s")
else:
logging.basicConfig(
level=logging.INFO,
handlers=[RichHandler(rich_tracebacks=True, markup=False, console=console, show_path=False, show_time=False)],
format=BARE_LOGGING_FORMAT,
)

logger = logging.getLogger("rich")
logging.getLogger("parso").setLevel(logging.WARNING)

# override the logger to reformat the messages for the lsp
for level in ("info", "debug", "warning", "error"):
real_fn = getattr(logger, level)
setattr(
logger,
level,
lambda msg, *args, _real_fn=real_fn, _level=level, **kwargs: enhanced_log(
msg, _real_fn, _level, *args, **kwargs
),
)
if not is_subagent_mode():
for level in ("info", "debug", "warning", "error"):
real_fn = getattr(logger, level)
setattr(
logger,
level,
lambda msg, *args, _real_fn=real_fn, _level=level, **kwargs: enhanced_log(
msg, _real_fn, _level, *args, **kwargs
),
)


class DummyTask:
Expand All @@ -87,6 +114,8 @@ def paneled_text(
text: str, panel_args: dict[str, str | bool] | None = None, text_args: dict[str, str] | None = None
) -> None:
"""Print text in a panel."""
if is_subagent_mode():
return
from rich.panel import Panel
from rich.text import Text

Expand Down Expand Up @@ -115,6 +144,8 @@ def code_print(
language: Programming language for syntax highlighting ('python', 'javascript', 'typescript')

"""
if is_subagent_mode():
return
if is_LSP_enabled():
lsp_log(
LspCodeMessage(code=code_str, file_name=file_name, function_name=function_name, message_id=lsp_message_id)
Expand Down Expand Up @@ -152,6 +183,10 @@ def progress_bar(
"""
global _progress_bar_active

if is_subagent_mode():
yield DummyTask().id
return

if is_LSP_enabled():
lsp_log(LspTextMessage(text=message, takes_time=True))
yield
Expand Down Expand Up @@ -183,6 +218,10 @@ def progress_bar(
@contextmanager
def test_files_progress_bar(total: int, description: str) -> Generator[tuple[Progress, TaskID], None, None]:
"""Progress bar for test files."""
if is_subagent_mode():
yield DummyProgress(), DummyTask().id
return

if is_LSP_enabled():
lsp_log(LspTextMessage(text=description, takes_time=True))
dummy_progress = DummyProgress()
Expand Down Expand Up @@ -216,6 +255,10 @@ def call_graph_live_display(
from rich.text import Text
from rich.tree import Tree

if is_subagent_mode():
yield lambda _: None
return

if is_LSP_enabled():
lsp_log(LspTextMessage(text="Building call graph", takes_time=True))
yield lambda _: None
Expand Down Expand Up @@ -323,6 +366,9 @@ def call_graph_summary(call_graph: DependencyResolver, file_to_funcs: dict[Path,
if not total_functions:
return

if is_subagent_mode():
return

# Build the mapping expected by the dependency resolver
file_items = file_to_funcs.items()
mapping = {file_path: {func.qualified_name for func in funcs} for file_path, funcs in file_items}
Expand All @@ -349,3 +395,87 @@ def call_graph_summary(call_graph: DependencyResolver, file_to_funcs: dict[Path,
return

console.print(Panel(summary, title="Call Graph Summary", border_style="cyan"))


def subagent_log_optimization_result(
function_name: str,
file_path: Path,
perf_improvement_line: str,
original_runtime_ns: int,
best_runtime_ns: int,
raw_explanation: str,
original_code: dict[Path, str],
new_code: dict[Path, str],
review: str,
test_results: TestResults,
) -> None:
import sys
from xml.sax.saxutils import escape

from codeflash.code_utils.code_utils import unified_diff_strings
from codeflash.code_utils.time_utils import humanize_runtime
from codeflash.models.test_type import TestType

diff_parts = []
for path in original_code:
old = original_code.get(path, "")
new = new_code.get(path, "")
if old != new:
diff = unified_diff_strings(old, new, fromfile=str(path), tofile=str(path))
if diff:
diff_parts.append(diff)

diff_str = "\n".join(diff_parts)

original_runtime = humanize_runtime(original_runtime_ns)
optimized_runtime = humanize_runtime(best_runtime_ns)

report = test_results.get_test_pass_fail_report_by_type()
verification_rows = []
for test_type in TestType:
if test_type is TestType.INIT_STATE_TEST:
continue
name = test_type.to_name()
if not name:
continue
passed = report[test_type]["passed"]
failed = report[test_type]["failed"]
if passed == 0 and failed == 0:
status = "None Found"
elif failed > 0:
status = f"{failed} Failed, {passed} Passed"
else:
status = f"{passed} Passed"
verification_rows.append(f' <test type="{escape(name)}" status="{escape(status)}"/>')

xml = [
"<codeflash-optimization>",
f" <function>{escape(function_name)}</function>",
f" <file>{escape(str(file_path))}</file>",
f" <performance>{escape(perf_improvement_line)}</performance>",
f" <original-runtime>{escape(original_runtime)}</original-runtime>",
f" <optimized-runtime>{escape(optimized_runtime)}</optimized-runtime>",
]
if review:
xml.append(f" <reviewer-confidence>{escape(review)}</reviewer-confidence>")
xml.append(f" <explanation>{escape(raw_explanation.strip())}</explanation>")
xml.append(" <verification>")
xml.extend(verification_rows)
xml.append(" </verification>")
if diff_str:
xml.append(f" <diff>{escape(diff_str)}</diff>")
for path in new_code:
if new_code[path] != original_code.get(path, ""):
xml.append(f' <optimized-code file="{escape(str(path))}">{escape(new_code[path])}</optimized-code>')
xml.append(" <action>")
xml.append(" Present this optimization to the user using AskUserQuestion.")
xml.append(" Show the function name, performance improvement, and explanation as the question text.")
xml.append(" Provide two options: 'Apply' (write the optimized code to the file) and 'Reject' (do nothing).")
xml.append(
" Put the full diff in the 'Apply' option's markdown preview so the user can review the exact changes."
)
xml.append(" If the user chooses 'Apply', write the content from optimized-code to the corresponding file.")
xml.append(" </action>")
xml.append("</codeflash-optimization>")

sys.stdout.write("\n".join(xml) + "\n")
10 changes: 10 additions & 0 deletions codeflash/cli_cmds/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@

def set_level(level: int, *, echo_setting: bool = True) -> None:
import logging
import sys
import time

from codeflash.lsp.helpers import is_subagent_mode

if is_subagent_mode():
logging.basicConfig(
level=level, handlers=[logging.StreamHandler(sys.stderr)], format="%(levelname)s: %(message)s", force=True
)
logging.getLogger().setLevel(level)
return

from rich.logging import RichHandler

from codeflash.cli_cmds.console import console
Expand Down
14 changes: 10 additions & 4 deletions codeflash/code_utils/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,18 @@ def get_all_historical_functions(module_root: Path, checkpoint_dir: Path) -> dic

def ask_should_use_checkpoint_get_functions(args: argparse.Namespace) -> Optional[dict[str, dict[str, str]]]:
previous_checkpoint_functions = None
if getattr(args, "subagent", False):
console.rule()
return None
if args.all and codeflash_temp_dir.is_dir():
previous_checkpoint_functions = get_all_historical_functions(args.module_root, codeflash_temp_dir)
if previous_checkpoint_functions and Confirm.ask(
"Previous Checkpoint detected from an incomplete optimization run, shall I continue the optimization from that point?",
default=True,
console=console,
if previous_checkpoint_functions and (
getattr(args, "yes", False)
or Confirm.ask(
"Previous Checkpoint detected from an incomplete optimization run, shall I continue the optimization from that point?",
default=True,
console=console,
)
):
console.rule()
else:
Expand Down
5 changes: 5 additions & 0 deletions codeflash/lsp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def is_LSP_enabled() -> bool:
return os.getenv("CODEFLASH_LSP", default="false").lower() == "true"


@lru_cache(maxsize=1)
def is_subagent_mode() -> bool:
return os.getenv("CODEFLASH_SUBAGENT_MODE", default="false").lower() == "true"


def tree_to_markdown(tree: Tree, level: int = 0) -> str:
"""Convert a rich Tree into a Markdown bullet list."""
indent = " " * level
Expand Down
6 changes: 6 additions & 0 deletions codeflash/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
from pathlib import Path
from typing import TYPE_CHECKING

if "--subagent" in sys.argv:
os.environ["CODEFLASH_SUBAGENT_MODE"] = "true"
import warnings

warnings.filterwarnings("ignore")

from codeflash.cli_cmds.cli import parse_args, process_pyproject_config
from codeflash.cli_cmds.cmd_init import CODEFLASH_LOGO, ask_run_end_to_end_test
from codeflash.cli_cmds.console import paneled_text
Expand Down
Loading
Loading