From fa57be6bbb90dfe3e4de3cb922fd642a4d140137 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 23 May 2026 13:05:15 -0700 Subject: [PATCH] [eric] tree: group foundational agent modules under agents/core --- backend/apps/agents/agent_manager.py | 8 +-- backend/apps/agents/agents.py | 12 ++--- backend/apps/agents/browser_agent.py | 4 +- backend/apps/agents/cloud_sync.py | 2 +- backend/apps/agents/core/__init__.py | 0 backend/apps/agents/{ => core}/aux_llm.py | 0 .../apps/agents/{ => core}/error_classify.py | 0 .../apps/agents/{ => core}/mcp_preflight.py | 0 backend/apps/agents/{ => core}/models.py | 0 .../agents/{ => core}/openai_passthrough.py | 0 backend/apps/agents/{ => core}/seq_log.py | 0 backend/apps/agents/{ => core}/ws_manager.py | 2 +- backend/apps/agents/session_store.py | 2 +- backend/apps/dashboards/dashboards.py | 2 +- backend/apps/nine_router/process.py | 2 +- backend/apps/outputs/outputs.py | 2 +- backend/main.py | 12 ++--- backend/tests/test_disconnect_resilience.py | 10 ++-- backend/tests/test_phase1_stress.py | 8 +-- backend/tests/test_service_legacy.py | 2 +- backend/tests/test_v2_invariants.py | 54 +++++++++---------- 21 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 backend/apps/agents/core/__init__.py rename backend/apps/agents/{ => core}/aux_llm.py (100%) rename backend/apps/agents/{ => core}/error_classify.py (100%) rename backend/apps/agents/{ => core}/mcp_preflight.py (100%) rename backend/apps/agents/{ => core}/models.py (100%) rename backend/apps/agents/{ => core}/openai_passthrough.py (100%) rename backend/apps/agents/{ => core}/seq_log.py (100%) rename backend/apps/agents/{ => core}/ws_manager.py (99%) diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 9dec92bd..d5d3d17c 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -9,10 +9,10 @@ from datetime import datetime from uuid import uuid4 from typing import Optional -from backend.apps.agents.models import ( +from backend.apps.agents.core.models import ( AgentConfig, AgentSession, Message, MessageBranch, ApprovalRequest, ToolGroupMeta, ) -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.settings.settings import load_settings from backend.apps.tools_lib.tools_lib import ( _load_all as load_all_tools, @@ -26,7 +26,7 @@ from backend.apps.tools_lib.tools_lib import ( save_trusted_sensitive_paths, ) from backend.config.paths import SESSIONS_DIR -from backend.apps.agents.error_classify import ( +from backend.apps.agents.core.error_classify import ( _NON_TRANSIENT_PATTERNS, _TRANSIENT_CAPACITY_PATTERNS, _is_auth_error, @@ -48,7 +48,7 @@ from backend.apps.agents.tool_catalog import ( _get_denied_tool_names, _is_fully_denied, ) -from backend.apps.agents.aux_llm import _safe_resp_text +from backend.apps.agents.core.aux_llm import _safe_resp_text from backend.apps.agents.history_compaction import ( _build_history_prefix, _get_branch_messages, diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index b6fff805..a90393da 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -1,7 +1,7 @@ from backend.config.Apps import SubApp from backend.apps.agents.agent_manager import agent_manager -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.models import AgentConfig, ApprovalResponse +from backend.apps.agents.core.ws_manager import ws_manager +from backend.apps.agents.core.models import AgentConfig, ApprovalResponse from contextlib import asynccontextmanager from fastapi import WebSocket, WebSocketDisconnect, HTTPException from fastapi.responses import JSONResponse @@ -67,8 +67,8 @@ async def send_message(session_id: str, body: dict): # Run MCP-suggestion classifier in parallel with the agent launch; fails open. try: - from backend.apps.agents.mcp_preflight import run_preflight - from backend.apps.agents.ws_manager import ws_manager as _ws + from backend.apps.agents.core.mcp_preflight import run_preflight + from backend.apps.agents.core.ws_manager import ws_manager as _ws async def _emit_preflight(): try: @@ -271,7 +271,7 @@ async def compact_session(session_id: str): raise HTTPException(status_code=404, detail="session not found") fired = agent_manager._maybe_compact(session, force=True) if fired: - from backend.apps.agents.ws_manager import ws_manager + from backend.apps.agents.core.ws_manager import ws_manager try: await ws_manager.send_to_session(session_id, "agent:context_status", { "session_id": session_id, @@ -296,7 +296,7 @@ async def clear_session(session_id: str): session.compacted_through_msg_id = None session.tokens = {"input": 0, "output": 0} session.needs_fresh_session = True - from backend.apps.agents.ws_manager import ws_manager + from backend.apps.agents.core.ws_manager import ws_manager try: await ws_manager.send_to_session(session_id, "agent:status", { "session_id": session_id, diff --git a/backend/apps/agents/browser_agent.py b/backend/apps/agents/browser_agent.py index a9463f4b..361cdcb0 100644 --- a/backend/apps/agents/browser_agent.py +++ b/backend/apps/agents/browser_agent.py @@ -38,8 +38,8 @@ from backend.apps.agents.browser_schema import ( MODEL_MAP, SYSTEM_PROMPT, ) -from backend.apps.agents.models import AgentSession, ApprovalRequest, Message -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.core.models import AgentSession, ApprovalRequest, Message +from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.tools_lib.tools_lib import load_builtin_permissions logger = logging.getLogger(__name__) diff --git a/backend/apps/agents/cloud_sync.py b/backend/apps/agents/cloud_sync.py index 5359f578..09c4e5af 100644 --- a/backend/apps/agents/cloud_sync.py +++ b/backend/apps/agents/cloud_sync.py @@ -1,6 +1,6 @@ from datetime import datetime -from backend.apps.agents.models import AgentSession +from backend.apps.agents.core.models import AgentSession from backend.apps.service.client import sync as _sync diff --git a/backend/apps/agents/core/__init__.py b/backend/apps/agents/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/agents/aux_llm.py b/backend/apps/agents/core/aux_llm.py similarity index 100% rename from backend/apps/agents/aux_llm.py rename to backend/apps/agents/core/aux_llm.py diff --git a/backend/apps/agents/error_classify.py b/backend/apps/agents/core/error_classify.py similarity index 100% rename from backend/apps/agents/error_classify.py rename to backend/apps/agents/core/error_classify.py diff --git a/backend/apps/agents/mcp_preflight.py b/backend/apps/agents/core/mcp_preflight.py similarity index 100% rename from backend/apps/agents/mcp_preflight.py rename to backend/apps/agents/core/mcp_preflight.py diff --git a/backend/apps/agents/models.py b/backend/apps/agents/core/models.py similarity index 100% rename from backend/apps/agents/models.py rename to backend/apps/agents/core/models.py diff --git a/backend/apps/agents/openai_passthrough.py b/backend/apps/agents/core/openai_passthrough.py similarity index 100% rename from backend/apps/agents/openai_passthrough.py rename to backend/apps/agents/core/openai_passthrough.py diff --git a/backend/apps/agents/seq_log.py b/backend/apps/agents/core/seq_log.py similarity index 100% rename from backend/apps/agents/seq_log.py rename to backend/apps/agents/core/seq_log.py diff --git a/backend/apps/agents/ws_manager.py b/backend/apps/agents/core/ws_manager.py similarity index 99% rename from backend/apps/agents/ws_manager.py rename to backend/apps/agents/core/ws_manager.py index 49fdd848..f2719b42 100644 --- a/backend/apps/agents/ws_manager.py +++ b/backend/apps/agents/core/ws_manager.py @@ -3,7 +3,7 @@ import json import logging from fastapi import WebSocket -from backend.apps.agents.seq_log import TERMINAL_STATUSES, seq_log +from backend.apps.agents.core.seq_log import TERMINAL_STATUSES, seq_log logger = logging.getLogger(__name__) diff --git a/backend/apps/agents/session_store.py b/backend/apps/agents/session_store.py index 4c095965..b23ba6ad 100644 --- a/backend/apps/agents/session_store.py +++ b/backend/apps/agents/session_store.py @@ -1,7 +1,7 @@ import json import os -from backend.apps.agents.models import AgentSession +from backend.apps.agents.core.models import AgentSession def _sessions_dir() -> str: diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index beec398a..208ac85a 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -338,7 +338,7 @@ async def generate_name(dashboard_id: str): system=system, messages=[{"role": "user", "content": user_content}], ) - from backend.apps.agents.aux_llm import _safe_resp_text + from backend.apps.agents.core.aux_llm import _safe_resp_text generated = _safe_resp_text(resp).strip().strip('"\'') if generated: fallback = generated diff --git a/backend/apps/nine_router/process.py b/backend/apps/nine_router/process.py index 379f902e..6578a079 100644 --- a/backend/apps/nine_router/process.py +++ b/backend/apps/nine_router/process.py @@ -39,7 +39,7 @@ NINE_ROUTER_V1 = f"{NINE_ROUTER_URL}/v1" # # Note: 0.3.60-0.4.20 ALL emit `max_tokens` (not max_completion_tokens) # when translating Anthropic→OpenAI, which OpenAI's GPT-5 family rejects. -# The fix lives in our /api/openai-passthrough proxy; see openai_passthrough.py +# The fix lives in our /api/openai-passthrough proxy; see core/openai_passthrough.py # and sync_openai_api_key for how the translation lane is rerouted via an # `openai-compatible` provider-node that honors `baseUrl`. NINE_ROUTER_NPM_VERSION = "0.3.60" diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index 8a9fa667..118e44c6 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -610,7 +610,7 @@ async def vibe_code(body: VibeCodeRequest): system=VIBE_CODE_SYSTEM_PROMPT, messages=[{"role": "user", "content": user_message}], ) - from backend.apps.agents.aux_llm import _safe_resp_text + from backend.apps.agents.core.aux_llm import _safe_resp_text raw = _safe_resp_text(resp).strip() if not raw: return { diff --git a/backend/main.py b/backend/main.py index 6b286c76..bdcbd709 100644 --- a/backend/main.py +++ b/backend/main.py @@ -18,7 +18,7 @@ from backend.apps.oauth_state import ( from backend.config.Apps import MainApp from backend.apps.health.health import health from backend.apps.agents.agents import agents -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.skills.skills import skills from backend.apps.tools_lib.tools_lib import tools_lib from backend.apps.modes.modes import modes @@ -175,7 +175,7 @@ async def websocket_session(websocket: WebSocket, session_id: str): last_seq = int(payload.get("last_seq") or 0) connection_uuid = payload.get("connection_uuid") or "" ack = await ws_manager.replay_to(session_id, websocket, last_seq) - from backend.apps.agents.seq_log import seq_log as _sl + from backend.apps.agents.core.seq_log import seq_log as _sl await websocket.send_text(json.dumps({ "event": "server:hello", "session_id": session_id, @@ -636,7 +636,7 @@ async def mcp_meta(action: str, request: Request): if session.sdk_session_id: session.needs_fresh_session = True try: - from backend.apps.agents.ws_manager import ws_manager as _ws + from backend.apps.agents.core.ws_manager import ws_manager as _ws await _ws.send_to_session(parent_session_id, "agent:status", { "session_id": parent_session_id, "status": session.status, @@ -703,7 +703,7 @@ async def session_compact(session_id: str): options and ships the compacted prefix. """ from backend.apps.agents.agent_manager import agent_manager - from backend.apps.agents.ws_manager import ws_manager as _ws + from backend.apps.agents.core.ws_manager import ws_manager as _ws session = agent_manager.sessions.get(session_id) if not session: return JSONResponse({"error": "session not found"}, status_code=404) @@ -721,8 +721,8 @@ async def session_compact(session_id: str): async def session_clear(session_id: str): """Wipe the session's UI history AND its SDK convo state (/clear slash cmd, Reset history button).""" from backend.apps.agents.agent_manager import agent_manager - from backend.apps.agents.ws_manager import ws_manager as _ws - from backend.apps.agents.models import MessageBranch + from backend.apps.agents.core.ws_manager import ws_manager as _ws + from backend.apps.agents.core.models import MessageBranch session = agent_manager.sessions.get(session_id) if not session: return JSONResponse({"error": "session not found"}, status_code=404) diff --git a/backend/tests/test_disconnect_resilience.py b/backend/tests/test_disconnect_resilience.py index 9f078819..511413f4 100644 --- a/backend/tests/test_disconnect_resilience.py +++ b/backend/tests/test_disconnect_resilience.py @@ -56,14 +56,14 @@ os.makedirs(_SEQ_DIR, exist_ok=True) @pytest.fixture(autouse=True) def _patch_persist_dir(): """Force the seq_log to use our tmp dir so we can assert on disk state.""" - from backend.apps.agents import seq_log as sl_mod + from backend.apps.agents.core import seq_log as sl_mod # Rebuild the singleton with our test dir. new_store = sl_mod.SeqLogStore(persist_dir=_SEQ_DIR) monkey = patch.object(sl_mod, "seq_log", new_store) monkey.start() # Also patch the symbol re-exported into ws_manager's import scope. - from backend.apps.agents import ws_manager as wm_mod + from backend.apps.agents.core import ws_manager as wm_mod wm_monkey = patch.object(wm_mod, "seq_log", new_store) wm_monkey.start() yield new_store @@ -84,7 +84,7 @@ def _build_app(seq_log): so the test thread can drive event emission through the same event loop as the WS handler, avoiding the cross-loop hazards of `asyncio.run()` mid-test.""" - from backend.apps.agents.ws_manager import ws_manager + from backend.apps.agents.core.ws_manager import ws_manager app = FastAPI() @@ -150,7 +150,7 @@ async def _emit_run(session_id: str, n_events: int, terminate: str | None = "com fanning out the broadcast across multiple coroutines. The seq log must still order them strictly. """ - from backend.apps.agents.ws_manager import ws_manager + from backend.apps.agents.core.ws_manager import ws_manager async def emit_chunk(start: int, count: int): for i in range(count): @@ -518,7 +518,7 @@ def test_disconnect_does_not_touch_agent_task(_patch_persist_dir): this test will catch it. We import agent_manager lazily so the `tasks` dict starts empty; we register a sentinel task and confirm disconnect_session doesn't poke it.""" - from backend.apps.agents.ws_manager import ws_manager + from backend.apps.agents.core.ws_manager import ws_manager # Insert a real Future into a parallel registry to mimic # `agent_manager.tasks[session_id]` and confirm ws_manager # never reaches into it. We don't import agent_manager (heavy); diff --git a/backend/tests/test_phase1_stress.py b/backend/tests/test_phase1_stress.py index b67c0961..c60aac36 100644 --- a/backend/tests/test_phase1_stress.py +++ b/backend/tests/test_phase1_stress.py @@ -34,7 +34,7 @@ os.environ.setdefault("OPENSWARM_DATA_DIR", _TMPROOT) def test_message_round_trips_client_id(): """The new field must default to None and survive model_dump.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message m = Message(role="user", content="hi") assert m.client_message_id is None @@ -52,7 +52,7 @@ def test_message_round_trips_client_id(): def test_message_legacy_payload_without_client_id(): """Older session JSON files won't have the field, must still load.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message legacy = { "id": "abc", @@ -68,7 +68,7 @@ def test_message_legacy_payload_without_client_id(): def test_client_message_id_collision_resistance(): """Many random client_message_ids must remain distinct values after serialization. Smoke-tests the field preservation in bulk.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message seen: set[str] = set() for _ in range(500): @@ -267,7 +267,7 @@ def test_notes_stress_many_round_trips(): async def test_concurrent_send_message_unique_client_ids(): """100 parallel Message constructions with unique client_message_ids must round-trip independently, no cross-talk on the dataclass.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message async def make_one(i: int): cmi = f"opt-{i}-{random.randint(0, 1_000_000)}" diff --git a/backend/tests/test_service_legacy.py b/backend/tests/test_service_legacy.py index 34bcc21c..f336b19e 100644 --- a/backend/tests/test_service_legacy.py +++ b/backend/tests/test_service_legacy.py @@ -136,7 +136,7 @@ def last_sync(kind: str) -> dict: # Import application modules (after fixtures are wired). from backend.apps.service.client import record -from backend.apps.agents.models import AgentConfig, AgentSession, Message, ApprovalRequest +from backend.apps.agents.core.models import AgentConfig, AgentSession, Message, ApprovalRequest from backend.apps.agents.agent_manager import AgentManager diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 8e7930a7..c01e4aa0 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -220,14 +220,14 @@ async def test_gate_stress_random_activations(): def test_needs_fresh_session_field_default_false(): """Brand-new sessions must default needs_fresh_session=False.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") assert s.needs_fresh_session is False def test_needs_fresh_session_serializes_round_trip(): """Pydantic round-trip must preserve the flag for session.json persistence.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.needs_fresh_session = True s.sdk_session_id = "claude-session-abc-123" @@ -240,7 +240,7 @@ def test_needs_fresh_session_serializes_round_trip(): def test_legacy_session_json_loads_without_field(): """Old session JSONs predate the field, Pydantic must fill in default.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession legacy = { "id": "old", "name": "legacy", "model": "sonnet", "mode": "agent", "status": "completed", "messages": [], @@ -255,7 +255,7 @@ def test_legacy_session_json_loads_without_field(): def test_mcp_activate_sets_fresh_session_when_history_exists(): """The gate logic at main.py: if sdk_session_id exists, set needs_fresh_session=True.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession # Mid-session: sdk already locked in s = AgentSession(id="mid", name="t", model="sonnet", mode="agent") s.sdk_session_id = "claude-session-existing" @@ -267,7 +267,7 @@ def test_mcp_activate_sets_fresh_session_when_history_exists(): def test_mcp_activate_skips_fresh_session_on_first_turn(): """First-turn activation: no sdk_session_id yet, so needs_fresh_session stays False.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="fresh", name="t", model="sonnet", mode="agent") # No sdk_session_id yet if s.sdk_session_id: @@ -277,7 +277,7 @@ def test_mcp_activate_skips_fresh_session_on_first_turn(): def test_active_mcps_append_idempotent(): """Activating the same server twice doesn't dupe.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.active_mcps.append("gmail") if "gmail" not in s.active_mcps: @@ -292,7 +292,7 @@ def test_active_mcps_append_idempotent(): def test_message_no_ghost_fields(): """answer_tokens + thought_signature must NOT be Message attributes anymore.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message m = Message(role="thinking", content="x") dumped = m.model_dump(mode="json") assert "answer_tokens" not in dumped @@ -301,7 +301,7 @@ def test_message_no_ghost_fields(): def test_message_legacy_payload_with_ghost_fields_still_loads(): """Old session JSONs may carry the deleted fields, Pydantic must ignore them.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message legacy = { "id": "m1", "role": "thinking", @@ -323,7 +323,7 @@ def test_message_legacy_payload_with_ghost_fields_still_loads(): def test_message_kept_fields(): """Verify the live fields remain on the model.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message m = Message( role="thinking", content="x", @@ -340,7 +340,7 @@ def test_message_kept_fields(): def test_message_round_trip_50_iterations(): """Stress: 50 randomized message round-trips.""" - from backend.apps.agents.models import Message + from backend.apps.agents.core.models import Message for _ in range(50): roles = ["user", "assistant", "tool_call", "tool_result", "system", "thinking"] m = Message( @@ -650,7 +650,7 @@ def test_mcp_activate_handler_unknown_server(): def test_active_mcps_persistence_on_session(): """active_mcps survives session.model_dump() round-trip, critical for resume.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.active_mcps = ["gmail", "slack"] dumped = json.dumps(s.model_dump(mode="json")) @@ -719,7 +719,7 @@ def test_chat_mode_not_in_builtins(): def test_active_mcps_default_factory_creates_new_list(): """Defaults must use Field(default_factory=list), not [], to avoid shared mutation.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s1 = AgentSession(id="a", name="a", model="sonnet", mode="agent") s2 = AgentSession(id="b", name="b", model="sonnet", mode="agent") s1.active_mcps.append("gmail") @@ -758,14 +758,14 @@ async def test_concurrent_gate_calls_isolated(): def test_pending_continuation_default_false(): - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") assert s.pending_continuation is False assert s.pending_continuation_prompt is None def test_pending_continuation_serializes(): - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.pending_continuation = True s.pending_continuation_prompt = "[mcp:auto-continue] retry now" @@ -777,7 +777,7 @@ def test_pending_continuation_serializes(): def test_compact_threshold_default(): """compact_threshold_pct default of 0.65, drift here breaks Phase 2 compaction.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") assert s.compact_threshold_pct == 0.65 assert s.context_soft_cap_pct == 0.90 @@ -999,7 +999,7 @@ def test_apply_context_window_overwrites_default_for_opus_4_7(): dataclass default for every model. _apply_context_window must pull the real 1M value from the registry for opus-4-7 / sonnet so the soft-cap trim and the % meter both reflect the real model cap.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession from backend.apps.agents.agent_manager import _apply_context_window s = AgentSession(id="x", name="t", model="opus-4-7", mode="agent") assert s.context_window == 200_000 @@ -1017,7 +1017,7 @@ def test_apply_context_window_silent_on_unknown_model(): """Bad lookup must NEVER raise; sessions with unknown/custom models that aren't in the registry fall back to the 128k registry default without breaking session creation.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession from backend.apps.agents.agent_manager import _apply_context_window s = AgentSession(id="x", name="t", model="nonexistent-model-xyz", mode="agent") _apply_context_window(s) @@ -1912,7 +1912,7 @@ def test_apply_context_window_respects_custom_provider_value(): """Custom OpenAI-compatible models supply their own context_window via settings.custom_providers. _apply_context_window must look them up the same way get_context_window does.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession from backend.apps.agents.agent_manager import _apply_context_window from backend.apps.settings.models import AppSettings, CustomProvider s = AgentSession(id="x", name="t", provider="custom", model="custom/ollama/qwen2.5:7b", mode="agent") @@ -2725,7 +2725,7 @@ def test_web_fetch_tool_has_name_and_schema(): def test_tool_group_meta_round_trip(): - from backend.apps.agents.models import ToolGroupMeta, AgentSession + from backend.apps.agents.core.models import ToolGroupMeta, AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.tool_group_meta["g1"] = ToolGroupMeta(id="g1", name="Reading files", svg="", is_refined=True) d = s.model_dump(mode="json") @@ -2735,7 +2735,7 @@ def test_tool_group_meta_round_trip(): def test_tool_group_meta_default_is_refined_false(): - from backend.apps.agents.models import ToolGroupMeta + from backend.apps.agents.core.models import ToolGroupMeta m = ToolGroupMeta(id="g", name="x") assert m.is_refined is False @@ -2746,14 +2746,14 @@ def test_tool_group_meta_default_is_refined_false(): def test_session_has_main_branch_by_default(): - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") assert "main" in s.branches assert s.active_branch_id == "main" def test_branch_serialization(): - from backend.apps.agents.models import AgentSession, MessageBranch + from backend.apps.agents.core.models import AgentSession, MessageBranch s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.branches["alt"] = MessageBranch(id="alt", parent_branch_id="main", fork_point_message_id="msg-1") d = s.model_dump(mode="json") @@ -2777,7 +2777,7 @@ async def test_e2e_session_lifecycle_with_mcp_activation(): 4. Persist & re-load, state survives """ from backend.apps.agents.agent_manager import AgentManager - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession fake_tools = [_fake_tool("Gmail"), _fake_tool("Slack")] with patch("backend.apps.agents.agent_manager.load_all_tools", return_value=fake_tools), \ patch("backend.apps.agents.agent_manager.refresh_google_token", new=AsyncMock(return_value=True)): @@ -2838,14 +2838,14 @@ async def test_e2e_50_random_activation_sequences(): def test_session_agent_active_ms_default_zero_for_legacy(): """A session loaded from JSON without `agent_active_ms` deserializes cleanly with default 0 (not None, not missing-key crash).""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(name="legacy", model="sonnet", mode="agent") assert s.agent_active_ms == 0 assert s.time_per_model == {} def test_session_agent_active_ms_round_trip(): - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(name="t", model="sonnet", mode="agent", agent_active_ms=12345, time_per_model={"haiku": 1000, "sonnet": 11345}) d = s.model_dump(mode="json") @@ -2857,7 +2857,7 @@ def test_session_agent_active_ms_round_trip(): def test_session_agent_active_ms_accumulates_via_dict_update(): """Simulates two turns adding to the bucket, the production accumulator pattern in agent_manager._on_result.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(name="t", model="sonnet", mode="agent") s.agent_active_ms = (s.agent_active_ms or 0) + 1500 s.time_per_model[s.model] = int(s.time_per_model.get(s.model, 0)) + 1500 @@ -2870,7 +2870,7 @@ def test_session_agent_active_ms_accumulates_via_dict_update(): def test_session_time_per_model_records_switch(): """Simulates a model switch mid-session, each model accumulates its own bucket.""" - from backend.apps.agents.models import AgentSession + from backend.apps.agents.core.models import AgentSession s = AgentSession(name="t", model="haiku", mode="agent") # Turn 1 on haiku s.time_per_model[s.model] = int(s.time_per_model.get(s.model, 0)) + 1200