Files
EvoScientist-Multi/EvoScientist/cli/agent.py
T
Ziheng Zhang 4b6a969df2 refactor(agent): make create_cli_agent(config=, chat_model=) pure (#267)
* refactor(agent): make create_cli_agent(config=, chat_model=) pure

Re-applies the #183 purity refactor on top of the observation-memory
lifecycle that landed in #259, integrating the two cleanly.

create_cli_agent gains a pure path: when both `config` and `chat_model`
are passed it builds the agent entirely from locals and writes none of
the cached module globals (`_config`, `_chat_model`, `_chat_model_key`,
`_EvoScientist_agent`). `/model` commits the switch via
`set_active_config` / `set_chat_model_instance` only after a successful
build, so a failed rebuild leaves the session on the original model
(replaces the old snapshot/restore rollback).

Supporting changes:
- Extract `set_active_config` (write-half of `_ensure_config`),
  `_apply_env_from_config`, `_build_chat_model`, and
  `set_chat_model_instance`.
- Thread `cfg` / `chat_model` through `_get_default_middleware`,
  `_build_base_kwargs`, `load_mcp_and_build_kwargs`,
  `_maybe_swap_async_subagents`, and `_inject_subagent_middleware` so the
  pure path never falls back to the global-writing `_ensure_config()` /
  `_ensure_chat_model()`.
- Integrate with #259's memory middleware: subagent context-editing
  middleware binds the threaded `chat_model`, and the configured system
  prompt / memory controls read the threaded `cfg` (new threading vs the
  original #183, required because #259 made these paths read config).
- Consolidate `cfg` resolution to one `cfg if cfg is not None else
  _ensure_config()` at the top of each kwargs builder, matching the
  pattern already used in the other config-aware helpers.

* fix(agent): keep pure tool selector off global cache

* fix(model): apply config switch in place to preserve reference integrity

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
Co-authored-by: X-iZhang <zacharyzhang2022@gmail.com>
2026-06-08 18:38:13 +01:00

92 lines
2.8 KiB
Python

"""Agent loading and workspace helpers."""
import os
from datetime import datetime
from pathlib import Path
from ..paths import new_run_dir
def _shorten_path(path: str) -> str:
"""Shorten absolute path to relative path from current directory."""
if not path:
return path
try:
cwd = os.getcwd()
if path.startswith(cwd):
rel = path[len(cwd) :].lstrip(os.sep)
return (
os.path.join(os.path.basename(cwd), rel)
if rel
else os.path.basename(cwd)
)
return path
except Exception:
return path
def _deduplicate_run_name(name: str, runs_dir: Path | None = None) -> str:
"""Return *name* if available, otherwise *name_1*, *name_2*, etc."""
if runs_dir is None:
from ..paths import RUNS_DIR
runs_dir = RUNS_DIR
if not (runs_dir / name).exists():
return name
i = 1
while (runs_dir / f"{name}_{i}").exists():
i += 1
return f"{name}_{i}"
def _create_session_workspace(name: str | None = None) -> str:
"""Create a per-session workspace directory and return its path.
Args:
name: Optional human-friendly run name. Duplicates are resolved
by appending ``_1``, ``_2``, etc. Falls back to a timestamp
if *name* is None.
"""
if name:
from ..paths import RUNS_DIR
session_id = _deduplicate_run_name(name, RUNS_DIR)
else:
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
workspace_dir = str(new_run_dir(session_id))
os.makedirs(workspace_dir, exist_ok=True)
return workspace_dir
def _load_agent(
workspace_dir: str | None = None,
checkpointer=None,
config=None,
chat_model=None,
*,
on_mcp_progress=None,
):
"""Load the CLI agent with optional persistent checkpointer.
Args:
workspace_dir: Optional per-session workspace directory.
checkpointer: Optional LangGraph checkpointer (e.g. ``AsyncSqliteSaver``).
Falls back to ``InMemorySaver`` when ``None``.
config: Optional pre-loaded ``EvoScientistConfig``. Forwarded to
``create_cli_agent`` to avoid double config loading.
chat_model: Optional pre-built chat model. Forwarded to
``create_cli_agent``; combined with an explicit ``config`` it
selects the pure (no module-global write) build path.
on_mcp_progress: Optional per-server MCP progress callback.
Signature ``(event, server_name, detail) -> None``.
"""
from ..EvoScientist import create_cli_agent
return create_cli_agent(
workspace_dir=workspace_dir,
checkpointer=checkpointer,
config=config,
chat_model=chat_model,
on_mcp_progress=on_mcp_progress,
)