mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +02:00
[eric] agents: run/ decomposition follows conventions (naming, @typechecked+types, p-private)
This commit is contained in:
@@ -27,8 +27,8 @@ from backend.apps.agents.manager.AgentLaunchMixin import AgentLaunchMixin
|
||||
from backend.apps.agents.manager.MockAgentMixin import MockAgentMixin
|
||||
from backend.apps.agents.manager.RunSupportMixin import RunSupportMixin
|
||||
from backend.apps.agents.manager.run.error_cards import handle_run_error
|
||||
from backend.apps.agents.manager.run.turn_runner import TurnRunnerMixin
|
||||
from backend.apps.agents.manager.run.run_options import RunOptionsMixin
|
||||
from backend.apps.agents.manager.run.TurnRunnerMixin import TurnRunnerMixin
|
||||
from backend.apps.agents.manager.run.RunOptionsMixin import RunOptionsMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -122,7 +122,7 @@ class AgentManager(SessionLifecycleMixin, SessionPersistenceMixin, MessagingMixi
|
||||
# who want a prompt on every command can flip Bash to "ask" in the UI.
|
||||
try:
|
||||
(options, options_kwargs, prompt_content, p_stderr_buffer,
|
||||
global_settings) = await self.p_build_agent_options(
|
||||
global_settings) = await self.build_agent_options(
|
||||
session, session_id, prompt, prompt_content, builtin_perms,
|
||||
selected_browser_ids, selected_app_output_ids, selected_setting_ids,
|
||||
fork_session, p_router_model_id, p_api_type_for_session)
|
||||
@@ -131,7 +131,7 @@ class AgentManager(SessionLifecycleMixin, SessionPersistenceMixin, MessagingMixi
|
||||
|
||||
turn = TurnState()
|
||||
thinking = ThinkingState()
|
||||
await self.p_run_turn_with_retry(
|
||||
await self.run_turn_with_retry(
|
||||
session, session_id, prompt_content, options, options_kwargs,
|
||||
turn, thinking, p_stderr_buffer, resolved_model, api_type, global_settings,
|
||||
)
|
||||
|
||||
+16
-20
@@ -7,7 +7,10 @@ the gate hooks resolve across the MRO unchanged."""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
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_tools, sanitize_server_name
|
||||
@@ -25,17 +28,23 @@ from backend.apps.agents.manager.prompt.system_prompt import compose_turn_system
|
||||
from backend.apps.agents.manager.prompt.tool_catalog import get_all_tool_names
|
||||
from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
|
||||
from backend.apps.agents.manager.run.run_options_helpers import (
|
||||
pre_send_context_guard, register_web_mcp_server, append_web_tools_hint, inject_thinking_options,
|
||||
pre_send_context_guard, set_framework_overhead, register_web_mcp_server,
|
||||
append_web_tools_hint, inject_thinking_options,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunOptionsMixin:
|
||||
async def p_build_agent_options(self, session, session_id, prompt, prompt_content,
|
||||
builtin_perms, selected_browser_ids, selected_app_output_ids,
|
||||
selected_setting_ids, fork_session, p_router_model_id,
|
||||
p_api_type_for_session):
|
||||
# No return annotation: the returned tuple carries an SDK ClaudeAgentOptions, which can't be
|
||||
# module-imported here (mock-mode would fail to import the manager); it's lazy-imported below.
|
||||
@typechecked
|
||||
async def build_agent_options(self, session: AgentSession, session_id: str, prompt: str,
|
||||
prompt_content: Union[str, List], builtin_perms: Dict[str, str],
|
||||
selected_browser_ids: Optional[List[str]],
|
||||
selected_app_output_ids: Optional[List[str]],
|
||||
selected_setting_ids: Optional[List[str]], fork_session: bool,
|
||||
p_router_model_id: str, p_api_type_for_session: str):
|
||||
from claude_agent_sdk import ClaudeAgentOptions
|
||||
from claude_agent_sdk.types import HookMatcher
|
||||
|
||||
@@ -92,20 +101,7 @@ class RunOptionsMixin:
|
||||
selected_setting_ids,
|
||||
)
|
||||
|
||||
# Per-turn estimate of framework overhead (subtracted from displayed
|
||||
# input). Conservative on purpose so honest over-shows beat lies.
|
||||
# 16K Claude Code preset, 12K base+deferred tools, ~3K/MCP (real
|
||||
# MCP tool definitions range 1-10K depending on server; 3K is a
|
||||
# rough median that keeps the meter honest without over-trimming),
|
||||
# char/4 of composed prompt.
|
||||
p_PRESET_OVERHEAD = 16_000
|
||||
p_TOOL_DEFS_OVERHEAD = 12_000
|
||||
p_PER_MCP_OVERHEAD = 3_000
|
||||
p_composed_tokens = len(composed_prompt or "") // 4
|
||||
p_mcp_tokens = len(session.active_mcps) * p_PER_MCP_OVERHEAD
|
||||
session.framework_overhead_tokens = (
|
||||
p_PRESET_OVERHEAD + p_TOOL_DEFS_OVERHEAD + p_composed_tokens + p_mcp_tokens
|
||||
)
|
||||
set_framework_overhead(session, composed_prompt)
|
||||
|
||||
# Pass session.active_mcps as the activation filter. Empty list ⇒
|
||||
# no MCP tools shipped to the SDK; the model must MCPSearch and
|
||||
@@ -163,7 +159,7 @@ class RunOptionsMixin:
|
||||
# SDK's ProcessError only stringifies to "Command failed with
|
||||
# exit code 1 / Check stderr output for details", which masks
|
||||
# transient capacity issues.
|
||||
p_stderr_buffer: list[str] = []
|
||||
p_stderr_buffer: List[str] = []
|
||||
|
||||
def p_stderr_cb(line: str) -> None:
|
||||
p_stderr_buffer.append(line)
|
||||
+13
-3
@@ -6,23 +6,33 @@ except-handlers can still read them after a mid-stream failure."""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Union
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
from backend.apps.agents.core.error_classify import CAPACITY_BACKOFFS, capacity_retry_wait
|
||||
from backend.apps.agents.manager.streaming.state import ThinkingState, TurnState
|
||||
from backend.apps.agents.manager.streaming import (
|
||||
stream_event,
|
||||
assistant_message,
|
||||
result_message,
|
||||
thinking as thinking_mod,
|
||||
)
|
||||
from backend.apps.settings.models import AppSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TurnRunnerMixin:
|
||||
async def p_run_turn_with_retry(self, session, session_id, prompt_content, options,
|
||||
options_kwargs, turn, thinking, p_stderr_buffer,
|
||||
resolved_model, api_type, global_settings):
|
||||
# `options` is the SDK ClaudeAgentOptions, lazy-imported below (so mock-mode can import the
|
||||
# manager without the SDK present), so it's left unannotated; everything else is typed.
|
||||
@typechecked
|
||||
async def run_turn_with_retry(self, session: AgentSession, session_id: str,
|
||||
prompt_content: Union[str, List], options,
|
||||
options_kwargs: Dict, turn: TurnState, thinking: ThinkingState,
|
||||
p_stderr_buffer: List[str], resolved_model: str, api_type: str,
|
||||
global_settings: AppSettings) -> None:
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
|
||||
from claude_agent_sdk.types import StreamEvent, SystemMessage
|
||||
|
||||
@@ -4,10 +4,13 @@ emits the matching system message + WS event. Pulled out of agent_manager so the
|
||||
the file ceiling; pure relocation, no self (operates on the passed run state)."""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import Message
|
||||
from backend.apps.agents.core.models import AgentSession, Message
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
from backend.apps.settings.settings import load_settings
|
||||
from backend.apps.agents.manager.streaming.state import TurnState
|
||||
from backend.apps.agents.core.error_classify import (
|
||||
is_long_context_error,
|
||||
is_transient_capacity_error,
|
||||
@@ -21,7 +24,8 @@ from backend.apps.agents.core.error_classify import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def handle_run_error(e, session, session_id, turn, p_stderr_buffer) -> None:
|
||||
@typechecked
|
||||
async def handle_run_error(e: Exception, session: AgentSession, session_id: str, turn: TurnState, p_stderr_buffer: List[str]) -> None:
|
||||
logger.exception(f"Agent {session_id} error: {e}")
|
||||
session.status = "error"
|
||||
|
||||
@@ -85,7 +89,7 @@ async def handle_run_error(e, session, session_id, turn, p_stderr_buffer) -> Non
|
||||
from backend.apps.service.client import submit_diagnostic
|
||||
submit_diagnostic({
|
||||
"kind": "context_overflow",
|
||||
"where": "agent_manager.p_run_streaming_turn",
|
||||
"where": "manager.run.error_cards.handle_run_error",
|
||||
"session_id": session_id,
|
||||
"model": session.model,
|
||||
"provider": session.provider,
|
||||
|
||||
@@ -3,15 +3,20 @@ assembly so each file stays under the ceiling. Free functions taking the manager
|
||||
emit_context_update); pure relocation."""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import Message
|
||||
from backend.apps.agents.core.models import AgentSession, Message
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
from backend.apps.agents.manager.session.history_compaction import estimate_post_compact_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def pre_send_context_guard(manager, session, session_id) -> None:
|
||||
# `manager` is the AgentManager; it isn't annotated because typing it would import agent_manager
|
||||
# back into a module agent_manager already imports (a cycle). Same reason self is never annotated.
|
||||
@typechecked
|
||||
async def pre_send_context_guard(manager, session: AgentSession, session_id: str) -> None:
|
||||
try:
|
||||
if manager.maybe_compact(session):
|
||||
new_input = estimate_post_compact_input(session)
|
||||
@@ -42,7 +47,7 @@ async def pre_send_context_guard(manager, session, session_id) -> None:
|
||||
p_est_tokens = session.tokens.get("input", 0)
|
||||
p_hard_cap = int(session.context_window * session.context_soft_cap_pct)
|
||||
if p_est_tokens >= p_hard_cap:
|
||||
trimmed: list[str] = []
|
||||
trimmed: List[str] = []
|
||||
while p_est_tokens >= p_hard_cap and len(session.active_mcps) > 1:
|
||||
# Keep at least one MCP active so the model can
|
||||
# finish whatever it was doing; trim from oldest
|
||||
@@ -88,7 +93,23 @@ async def pre_send_context_guard(manager, session, session_id) -> None:
|
||||
logger.exception("pre-send token guard failed; proceeding")
|
||||
|
||||
|
||||
def register_web_mcp_server(mcp_servers, p_m) -> None:
|
||||
@typechecked
|
||||
def set_framework_overhead(session: AgentSession, composed_prompt: Optional[str]) -> None:
|
||||
"""Per-turn estimate of framework overhead (subtracted from displayed input). Conservative on
|
||||
purpose so honest over-shows beat lies: 16K Claude Code preset, 12K base+deferred tools, ~3K/MCP
|
||||
(real defs span 1-10K; 3K median keeps the meter honest), char/4 of the composed prompt."""
|
||||
p_PRESET_OVERHEAD = 16_000
|
||||
p_TOOL_DEFS_OVERHEAD = 12_000
|
||||
p_PER_MCP_OVERHEAD = 3_000
|
||||
p_composed_tokens = len(composed_prompt or "") // 4
|
||||
p_mcp_tokens = len(session.active_mcps) * p_PER_MCP_OVERHEAD
|
||||
session.framework_overhead_tokens = (
|
||||
p_PRESET_OVERHEAD + p_TOOL_DEFS_OVERHEAD + p_composed_tokens + p_mcp_tokens
|
||||
)
|
||||
|
||||
|
||||
@typechecked
|
||||
def register_web_mcp_server(mcp_servers: Dict, p_m: str) -> None:
|
||||
"""Register the DDG-backed openswarm-web stdio MCP into the server set when the primary has no
|
||||
reliable native web path. The server script lives in the agents package (not here), so resolve
|
||||
it off that package dir, not __file__."""
|
||||
@@ -121,7 +142,8 @@ def register_web_mcp_server(mcp_servers, p_m) -> None:
|
||||
)
|
||||
|
||||
|
||||
def append_web_tools_hint(composed_prompt, need_web_mcp, effective_allowed) -> str:
|
||||
@typechecked
|
||||
def append_web_tools_hint(composed_prompt: Optional[str], need_web_mcp: bool, effective_allowed: List[str]) -> str:
|
||||
"""Append a <web_tools> block naming the MCP-backed WebSearch/WebFetch when the deferred bare
|
||||
WebSearch tool isn't usable on this session, so smaller models don't thrash on ToolSearch."""
|
||||
p_web_tools_available = need_web_mcp and (
|
||||
@@ -158,7 +180,8 @@ def append_web_tools_hint(composed_prompt, need_web_mcp, effective_allowed) -> s
|
||||
return f"{composed_prompt}\n\n{p_web_hint}" if composed_prompt else p_web_hint
|
||||
|
||||
|
||||
def inject_thinking_options(options_kwargs, session, prompt, resolved_model, api_type) -> None:
|
||||
@typechecked
|
||||
def inject_thinking_options(options_kwargs: Dict, session: AgentSession, prompt: str, resolved_model: str, api_type: str) -> None:
|
||||
"""Map the session's thinking_level onto the SDK options (anthropic thinking/effort, openai/codex
|
||||
reasoning_effort), with the short-prompt + gc/gemini-3 force-off overrides. Best-effort."""
|
||||
try:
|
||||
|
||||
@@ -62,7 +62,7 @@ def p_capture_env(monkeypatch, settings, api_type, resolved_model, model_entry):
|
||||
into ClaudeAgentOptions (the provider-route auth config the SDK runs under)."""
|
||||
import backend.apps.agents.providers.registry as reg
|
||||
import backend.apps.agents.agent_manager as am
|
||||
import backend.apps.agents.manager.run.run_options as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
|
||||
monkeypatch.setattr(run_opts, "load_settings", lambda: settings, raising=True)
|
||||
monkeypatch.setattr(reg, "get_api_type", lambda model: api_type, raising=True)
|
||||
@@ -169,7 +169,7 @@ def test_loop_builds_direct_anthropic_key_env(monkeypatch):
|
||||
from backend.apps.settings.models import AppSettings
|
||||
import backend.apps.agents.providers.registry as reg
|
||||
import backend.apps.agents.agent_manager as am
|
||||
import backend.apps.agents.manager.run.run_options as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
|
||||
settings = AppSettings(anthropic_api_key="sk-ant-test123", connection_mode="own_key")
|
||||
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
|
||||
@@ -206,7 +206,7 @@ def test_loop_with_session_cwd_runs_workspace_git_init(monkeypatch):
|
||||
# sessions normally have no cwd, which masked a NameError (the call said ensure_cwd_git_repo
|
||||
# while only _ensure_cwd_git_repo was imported). raising=True here would fail if the name were
|
||||
# missing again; the assertions confirm the cwd path actually runs and the turn completes.
|
||||
import backend.apps.agents.manager.run.run_options as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
called = {}
|
||||
|
||||
def fake_ensure(cwd, home=None):
|
||||
|
||||
Reference in New Issue
Block a user