[eric] tree: group foundational agent modules under agents/core

This commit is contained in:
ciregenz
2026-05-23 13:05:15 -07:00
parent a978ba30ca
commit fa57be6bbb
21 changed files with 61 additions and 61 deletions
+4 -4
View File
@@ -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,
+6 -6
View File
@@ -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,
+2 -2
View File
@@ -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__)
+1 -1
View File
@@ -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
@@ -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__)
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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 {
+6 -6
View File
@@ -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)
+5 -5
View File
@@ -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);
+4 -4
View File
@@ -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)}"
+1 -1
View File
@@ -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
+27 -27
View File
@@ -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="<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