fix: lazy-load server_registry, goal judge and encrypted session store (fixes #6167)

Move three opt-in-only imports off module top level:
- agent/execution_mixin.py: _server_registry now imported in the launch
  path and cleanup_launch_registration().
- goal/loop.py: judge_goal/_tail imported inside _goal_gate().
- session/__init__.py: EncryptedSessionStore & co. served via __getattr__.

Tests now patch praisonaiagents.goal.judge.judge_goal (the lookup site).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
MervinPraisonandClaude Opus 5.5 committed 2026-10-31 18:29:27 +00:00
1 parent 1d68f23cfb
commit f8768debb4
6 files changed
+30 -28

No files matched your search

@@ -22,12 +22,6 @@ logger = logging.getLogger(__name__)
from typing import List, Optional, Any, Dict, Union, Generator, TYPE_CHECKING
# Shared HTTP launch state — the process-wide registry is defined in
# agent/server_registry.py and shared with PraisonAIAgents.launch() so a
# standalone Agent and a team launched on the same port cannot double-bind it or
# hide each other's endpoints.
from .server_registry import _server_registry
class _TimeoutCancelToken:
"""cancel_token for caller-mode ``start(timeout=...)``.
@@ -76,6 +70,8 @@ def cleanup_launch_registration(agent_id: str) -> None:
``_AgentServerRegistry.teardown_routes_for``) so cleanup cannot race a
concurrent launch that reserves and registers a route on the same port.
"""
from .server_registry import _server_registry # lazy: launch path only
_server_registry.teardown_routes_for(agent_id)
if TYPE_CHECKING:
@@ -2394,7 +2390,11 @@ Write the complete compiled report:"""
host = resolve_launch_host(host)
# Get or create the shared FastAPI app for this port (shared with
# PraisonAIAgents.launch via the same _server_registry singleton).
# PraisonAIAgents.launch via the same _server_registry singleton, so a
# standalone Agent and a team on one port cannot double-bind it).
# Imported lazily: only the HTTP launch path needs the registry.
from .server_registry import _server_registry
app, is_new_app = _server_registry.get_or_create_app(
port,
title=f"PraisonAI Agents API (Port {port})",
@@ -14,7 +14,6 @@ from typing import Any, Optional, Tuple, TYPE_CHECKING
from praisonaiagents._logging import get_logger
from .judge import judge_goal, _tail
if TYPE_CHECKING:
from .models import GoalCriteria, GoalState
@@ -168,6 +167,8 @@ class GoalLoopMixin:
self._persist_goal_state()
return "continue", reason
from .judge import judge_goal, _tail # lazy: goal loop is opt-in
state.turns_used += 1
verdict, reason = judge_goal(
state,
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
from .sqlite_transcript_store import SqliteTranscriptStore
from .protocols import SessionStoreProtocol, SessionMirrorProtocol
from .hierarchy import HierarchicalSessionStore, SessionSnapshot, ExtendedSessionData
from .encrypted_store import EncryptedSessionStore, SessionEncryptionError, generate_session_key
# Lazy loading for zero import overhead
_module_cache = {}
@@ -207,6 +208,12 @@ def __getattr__(name: str):
_module_cache[name] = wrap_inter_agent
return wrap_inter_agent
if name in ("EncryptedSessionStore", "SessionEncryptionError", "generate_session_key"):
from . import encrypted_store
value = getattr(encrypted_store, name)
_module_cache[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -249,9 +256,3 @@ __all__ = [
"SessionEncryptionError",
"generate_session_key",
]
from .encrypted_store import ( # noqa: E402
EncryptedSessionStore,
SessionEncryptionError,
generate_session_key,
)
@@ -173,7 +173,7 @@ class TestGoalLoopGate:
verdicts = iter([("continue", "not yet"), ("done", "goal met")])
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
side_effect=lambda *a, **k: next(verdicts)):
result = agent.run_goal("do the task", goal="finish it", max_turns=10)
@@ -193,7 +193,7 @@ class TestGoalLoopGate:
return f"still working {counter['n']}"
with patch.object(agent, "chat", side_effect=fake_chat), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
side_effect=lambda *a, **k: next(verdicts)):
result = agent.run_goal("task", goal="g", max_turns=10)
@@ -205,7 +205,7 @@ class TestGoalLoopGate:
def test_budget_exhaust_pauses_recoverably(self):
agent = _make_agent()
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
return_value=("continue", "never done")):
result = agent.run_goal("task", goal="g", max_turns=3)
@@ -218,7 +218,7 @@ class TestGoalLoopGate:
agent = _make_agent()
# judge_goal itself fails open to ("continue", "judge unavailable")
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
return_value=("continue", "judge unavailable")):
result = agent.run_goal("task", goal="g", max_turns=2)
assert result.completion_reason == "budget_paused"
@@ -226,7 +226,7 @@ class TestGoalLoopGate:
def test_consecutive_parse_failures_autopause(self):
agent = _make_agent()
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
return_value=("continue", "judge response unparseable")):
result = agent.run_goal("task", goal="g", max_turns=20)
# auto-pause after 3 consecutive parse failures, well before max_turns
@@ -259,7 +259,7 @@ class TestConstraintViolation:
)
# Simulate a judge that respects constraints: constraint violated → continue
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
return_value=("continue", "constraint violated")):
result = agent.run_goal("task", goal="g", criteria=criteria, max_turns=2)
assert result.completion_reason == "budget_paused"
@@ -282,7 +282,7 @@ class TestAsyncGoalGate:
state = GoalState(goal="g", max_turns=5)
state.status = "active"
agent._goal_state = state
with patch("praisonaiagents.goal.loop.judge_goal",
with patch("praisonaiagents.goal.judge.judge_goal",
return_value=("done", "goal met")):
outcome = asyncio.run(agent._goal_gate_async("final response"))
assert outcome == ("done", "goal met")
@@ -314,7 +314,7 @@ class TestAsyncGoalGate:
return ("done", "eventually met")
async def scenario():
with patch("praisonaiagents.goal.loop.judge_goal",
with patch("praisonaiagents.goal.judge.judge_goal",
side_effect=slow_judge):
task = asyncio.ensure_future(agent._goal_gate_async("resp"))
await asyncio.to_thread(started.wait, 2.0)
@@ -330,7 +330,7 @@ class TestAsyncGoalGate:
def test_run_until_with_goal_delegates(self):
agent = _make_agent()
with patch.object(agent, "chat", side_effect=_unique_chat()), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
return_value=("done", "met")):
result = agent.run_until("task", goal="reach the goal", max_iterations=5)
assert result.completion_reason == "goal_met"
@@ -7,7 +7,7 @@ at ``turns_used=0``. A crashed run (still ``active``) was also never resumed.
import pytest
import praisonaiagents.goal.loop as goal_loop
import praisonaiagents.goal.judge as goal_judge
from praisonaiagents import Agent
from praisonaiagents.goal.models import GoalState
from praisonaiagents.model_harness import ScriptedModel
@@ -20,7 +20,7 @@ GOAL = "all tests pass"
def _isolated_home(tmp_path, monkeypatch):
monkeypatch.setenv("PRAISONAI_HOME", str(tmp_path / ".praisonai"))
monkeypatch.setattr(
goal_loop, "judge_goal", lambda state, *a, **k: ("continue", "not yet")
goal_judge, "judge_goal", lambda state, *a, **k: ("continue", "not yet")
)
@@ -228,7 +228,7 @@ class TestGoalLoopEvidence:
with patch.object(agent, "chat",
side_effect=lambda p: f"step {counter.__setitem__('n', counter['n'] + 1) or counter['n']}"), \
patch("praisonaiagents.goal.loop.judge_goal", side_effect=fake_judge):
patch("praisonaiagents.goal.judge.judge_goal", side_effect=fake_judge):
result = agent.run_goal("task", goal="g", max_turns=2)
# Blocking hook fails every turn → judge never consulted, ends paused.
@@ -247,7 +247,7 @@ class TestGoalLoopEvidence:
return ("done", "met")
with patch.object(agent, "chat", side_effect=lambda pr: "did it"), \
patch("praisonaiagents.goal.loop.judge_goal", side_effect=fake_judge):
patch("praisonaiagents.goal.judge.judge_goal", side_effect=fake_judge):
result = agent.run_goal("task", goal="g", max_turns=5)
assert result.completion_reason == "goal_met"
@@ -283,7 +283,7 @@ class TestGoalLoopEvidence:
return "trying"
with patch.object(agent, "chat", side_effect=fake_chat), \
patch("praisonaiagents.goal.loop.judge_goal",
patch("praisonaiagents.goal.judge.judge_goal",
side_effect=lambda *a, **k: ("done", "met")):
result = agent.run_goal("task", goal="g", max_turns=2)