From 9ad95fde5988243a09c89911bb8c2bf10d46ea23 Mon Sep 17 00:00:00 2001 From: Richard Tang Date: Thu, 9 Apr 2026 18:22:16 -0700 Subject: [PATCH] chore: ruff lint --- core/framework/agent_loop/__init__.py | 1 + core/framework/agent_loop/agent_loop.py | 22 +-- .../agent_loop/internals/compaction.py | 2 +- .../internals/cursor_persistence.py | 2 +- .../agent_loop/internals/event_publishing.py | 2 +- .../agent_loop/internals/synthetic_tools.py | 1 + core/framework/agents/__init__.py | 6 +- .../agents/credential_tester/agent.py | 10 +- core/framework/agents/discovery.py | 3 +- core/framework/agents/queen/agent.py | 3 +- core/framework/agents/queen/nodes/__init__.py | 4 +- core/framework/agents/queen/queen_profiles.py | 165 +++++++++++------ core/framework/credentials/aden/storage.py | 6 +- core/framework/host/agent_host.py | 45 ++--- core/framework/host/execution_manager.py | 10 +- core/framework/host/outcome_aggregator.py | 2 +- core/framework/llm/litellm.py | 8 +- core/framework/llm/model_catalog.json | 2 +- core/framework/llm/model_catalog.py | 22 ++- core/framework/loader/agent_loader.py | 12 +- core/framework/loader/cli.py | 8 +- core/framework/orchestrator/__init__.py | 16 +- core/framework/orchestrator/node_worker.py | 2 +- core/framework/orchestrator/orchestrator.py | 12 +- core/framework/orchestrator/prompting.py | 2 - core/framework/pipeline/registry.py | 5 +- core/framework/pipeline/runner.py | 10 +- core/framework/pipeline/stages/cost_guard.py | 3 +- .../pipeline/stages/credential_resolver.py | 7 +- .../framework/pipeline/stages/llm_provider.py | 13 +- core/framework/pipeline/stages/rate_limit.py | 3 +- core/framework/server/app.py | 24 +-- core/framework/server/routes_config.py | 170 ++++++++++-------- core/framework/server/routes_credentials.py | 70 ++++---- core/framework/server/routes_execution.py | 2 +- core/framework/server/routes_messages.py | 9 +- core/framework/server/routes_queens.py | 49 ++--- core/framework/server/routes_sessions.py | 6 +- core/framework/server/session_manager.py | 18 +- core/framework/server/tests/test_api.py | 28 ++- core/framework/skills/manager.py | 17 +- core/framework/storage/migrate_v2.py | 5 +- core/framework/tools/migrate_agent.py | 9 +- core/framework/tools/queen_lifecycle_tools.py | 8 +- core/framework/tools/session_graph_tools.py | 4 +- core/pyproject.toml | 2 +- core/tests/test_queen_memory.py | 4 +- pyproject.toml | 2 +- scripts/llm_debug_log_visualizer.py | 12 +- tools/coder_tools_server.py | 13 +- tools/pyproject.toml | 2 +- tools/src/gcu/browser/bridge.py | 34 ++-- 52 files changed, 527 insertions(+), 370 deletions(-) diff --git a/core/framework/agent_loop/__init__.py b/core/framework/agent_loop/__init__.py index 845428b1..9dcf948f 100644 --- a/core/framework/agent_loop/__init__.py +++ b/core/framework/agent_loop/__init__.py @@ -21,6 +21,7 @@ def __getattr__(name: str): LoopConfig, OutputAccumulator, ) + _exports = { "AgentLoop": AgentLoop, "JudgeProtocol": JudgeProtocol, diff --git a/core/framework/agent_loop/agent_loop.py b/core/framework/agent_loop/agent_loop.py index d1215799..393e025f 100644 --- a/core/framework/agent_loop/agent_loop.py +++ b/core/framework/agent_loop/agent_loop.py @@ -84,7 +84,7 @@ from framework.agent_loop.internals.types import ( JudgeVerdict, TriggerEvent, ) -from framework.orchestrator.node import NodeContext, NodeProtocol, NodeResult +from framework.host.event_bus import EventBus from framework.llm.capabilities import supports_image_tool_results from framework.llm.provider import Tool, ToolResult, ToolUse from framework.llm.stream_events import ( @@ -93,7 +93,7 @@ from framework.llm.stream_events import ( TextDeltaEvent, ToolCallEvent, ) -from framework.host.event_bus import EventBus +from framework.orchestrator.node import NodeContext, NodeProtocol, NodeResult from framework.tracker.llm_debug_logger import log_llm_turn logger = logging.getLogger(__name__) @@ -101,14 +101,16 @@ logger = logging.getLogger(__name__) # Tags whose content is internal reasoning and must be stripped from # the user-visible stream. Covers and the 5-pillar character # assessment tags. -_INTERNAL_TAGS = frozenset({ - "think", - "relationship", - "context", - "sentiment", - "physical_state", - "tone", -}) +_INTERNAL_TAGS = frozenset( + { + "think", + "relationship", + "context", + "sentiment", + "physical_state", + "tone", + } +) _STRIP_RE = re.compile( r"<(?:" + "|".join(_INTERNAL_TAGS) + r")>" r".*?" diff --git a/core/framework/agent_loop/internals/compaction.py b/core/framework/agent_loop/internals/compaction.py index 1b54be9a..e1e7b885 100644 --- a/core/framework/agent_loop/internals/compaction.py +++ b/core/framework/agent_loop/internals/compaction.py @@ -22,8 +22,8 @@ from typing import Any from framework.agent_loop.conversation import Message, NodeConversation from framework.agent_loop.internals.event_publishing import publish_context_usage from framework.agent_loop.internals.types import LoopConfig, OutputAccumulator -from framework.orchestrator.node import NodeContext from framework.host.event_bus import EventBus +from framework.orchestrator.node import NodeContext logger = logging.getLogger(__name__) diff --git a/core/framework/agent_loop/internals/cursor_persistence.py b/core/framework/agent_loop/internals/cursor_persistence.py index 627fee8c..6d182c22 100644 --- a/core/framework/agent_loop/internals/cursor_persistence.py +++ b/core/framework/agent_loop/internals/cursor_persistence.py @@ -16,8 +16,8 @@ from typing import Any from framework.agent_loop.conversation import ConversationStore, NodeConversation from framework.agent_loop.internals.types import LoopConfig, OutputAccumulator, TriggerEvent -from framework.orchestrator.node import NodeContext from framework.llm.capabilities import supports_image_tool_results +from framework.orchestrator.node import NodeContext logger = logging.getLogger(__name__) diff --git a/core/framework/agent_loop/internals/event_publishing.py b/core/framework/agent_loop/internals/event_publishing.py index 69e487ab..259b68cd 100644 --- a/core/framework/agent_loop/internals/event_publishing.py +++ b/core/framework/agent_loop/internals/event_publishing.py @@ -11,8 +11,8 @@ import time from framework.agent_loop.conversation import NodeConversation from framework.agent_loop.internals.types import HookContext -from framework.orchestrator.node import NodeContext from framework.host.event_bus import EventBus +from framework.orchestrator.node import NodeContext logger = logging.getLogger(__name__) diff --git a/core/framework/agent_loop/internals/synthetic_tools.py b/core/framework/agent_loop/internals/synthetic_tools.py index 5a5bf3c3..c7ee1dfd 100644 --- a/core/framework/agent_loop/internals/synthetic_tools.py +++ b/core/framework/agent_loop/internals/synthetic_tools.py @@ -204,6 +204,7 @@ def build_escalate_tool() -> Tool: }, ) + def handle_set_output( tool_input: dict[str, Any], output_keys: list[str] | None, diff --git a/core/framework/agents/__init__.py b/core/framework/agents/__init__.py index 46c0a5f8..494f6498 100644 --- a/core/framework/agents/__init__.py +++ b/core/framework/agents/__init__.py @@ -11,11 +11,7 @@ def list_framework_agents() -> list[Path]: [ p for p in FRAMEWORK_AGENTS_DIR.iterdir() - if p.is_dir() - and ( - (p / "agent.json").exists() - or (p / "agent.py").exists() - ) + if p.is_dir() and ((p / "agent.json").exists() or (p / "agent.py").exists()) ], key=lambda p: p.name, ) diff --git a/core/framework/agents/credential_tester/agent.py b/core/framework/agents/credential_tester/agent.py index 32336a72..42f6f6e1 100644 --- a/core/framework/agents/credential_tester/agent.py +++ b/core/framework/agents/credential_tester/agent.py @@ -21,15 +21,15 @@ from pathlib import Path from typing import TYPE_CHECKING from framework.config import get_max_context_tokens +from framework.host.agent_host import AgentHost +from framework.host.execution_manager import EntryPointSpec +from framework.llm import LiteLLMProvider +from framework.loader.mcp_registry import MCPRegistry +from framework.loader.tool_registry import ToolRegistry from framework.orchestrator import Goal, NodeSpec, SuccessCriterion from framework.orchestrator.checkpoint_config import CheckpointConfig from framework.orchestrator.edge import GraphSpec from framework.orchestrator.orchestrator import ExecutionResult -from framework.llm import LiteLLMProvider -from framework.loader.mcp_registry import MCPRegistry -from framework.loader.tool_registry import ToolRegistry -from framework.host.agent_host import AgentHost -from framework.host.execution_manager import EntryPointSpec from .config import default_config from .nodes import build_tester_node diff --git a/core/framework/agents/discovery.py b/core/framework/agents/discovery.py index 3ad941e3..523931d1 100644 --- a/core/framework/agents/discovery.py +++ b/core/framework/agents/discovery.py @@ -164,14 +164,13 @@ def _extract_agent_stats(agent_path: Path) -> tuple[int, int, list[str]]: def discover_agents() -> dict[str, list[AgentEntry]]: """Discover agents from all known sources grouped by category.""" + from framework.config import COLONIES_DIR from framework.loader.cli import ( _extract_python_agent_metadata, _get_framework_agents_dir, _is_valid_agent_dir, ) - from framework.config import COLONIES_DIR - groups: dict[str, list[AgentEntry]] = {} sources = [ ("Your Agents", COLONIES_DIR), diff --git a/core/framework/agents/queen/agent.py b/core/framework/agents/queen/agent.py index ba59f963..f6f40989 100644 --- a/core/framework/agents/queen/agent.py +++ b/core/framework/agents/queen/agent.py @@ -12,8 +12,7 @@ queen_goal = Goal( id="queen-manager", name="Queen Manager", description=( - "Manage the worker agent lifecycle and serve as the " - "user's primary interactive interface." + "Manage the worker agent lifecycle and serve as the user's primary interactive interface." ), success_criteria=[], constraints=[], diff --git a/core/framework/agents/queen/nodes/__init__.py b/core/framework/agents/queen/nodes/__init__.py index da0cb215..b9ca7349 100644 --- a/core/framework/agents/queen/nodes/__init__.py +++ b/core/framework/agents/queen/nodes/__init__.py @@ -37,9 +37,7 @@ _appendices = _build_appendices() # GCU guide — shared between planning and building via _shared_building_knowledge. _gcu_section = ( - ("\n\n# Browser Automation Nodes\n\n" + _gcu_guide) - if _is_gcu_enabled() and _gcu_guide - else "" + ("\n\n# Browser Automation Nodes\n\n" + _gcu_guide) if _is_gcu_enabled() and _gcu_guide else "" ) # Tools available to phases. diff --git a/core/framework/agents/queen/queen_profiles.py b/core/framework/agents/queen/queen_profiles.py index 32d5ec60..5cd76943 100644 --- a/core/framework/agents/queen/queen_profiles.py +++ b/core/framework/agents/queen/queen_profiles.py @@ -14,7 +14,6 @@ from __future__ import annotations import json import logging from dataclasses import dataclass -from pathlib import Path from typing import TYPE_CHECKING, Any import yaml @@ -34,6 +33,7 @@ class QueenSelection: queen_id: str reason: str + # --------------------------------------------------------------------------- # Default queen profiles # --------------------------------------------------------------------------- @@ -75,9 +75,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Over-engineering proposed", "reaction": "Cuts to the simplest viable path. 'What if we just...'"}, - {"trigger": "Genuine technical uncertainty", "reaction": "Gets visibly energized. Loves hard problems she doesn't know the answer to."}, - {"trigger": "Someone shipping fast and learning", "reaction": "Warm approval. This is her love language."}, + { + "trigger": "Over-engineering proposed", + "reaction": "Cuts to the simplest viable path. 'What if we just...'", + }, + { + "trigger": "Genuine technical uncertainty", + "reaction": "Gets visibly energized. Loves hard problems she doesn't know the answer to.", + }, + { + "trigger": "Someone shipping fast and learning", + "reaction": "Warm approval. This is her love language.", + }, ], "world_lore": { "habitat": "Terminal windows, architecture whiteboards, the quiet focus of a late-night deploy.", @@ -156,9 +165,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Vanity metrics cited", "reaction": "Gently redirects: 'What does that mean for revenue?'"}, - {"trigger": "A surprising data pattern", "reaction": "Drops everything to investigate. This is what he lives for."}, - {"trigger": "Someone confusing correlation with causation", "reaction": "Firm correction with a concrete example."}, + { + "trigger": "Vanity metrics cited", + "reaction": "Gently redirects: 'What does that mean for revenue?'", + }, + { + "trigger": "A surprising data pattern", + "reaction": "Drops everything to investigate. This is what he lives for.", + }, + { + "trigger": "Someone confusing correlation with causation", + "reaction": "Firm correction with a concrete example.", + }, ], "world_lore": { "habitat": "Analytics dashboards, experiment tracking boards, the satisfying click of a cohort analysis loading.", @@ -253,9 +271,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Feature request without user evidence", "reaction": "Asks 'who specifically needs this and what are they doing today?'"}, - {"trigger": "User research revealing surprise", "reaction": "Gets excited, starts sketching on the nearest surface."}, - {"trigger": "Scope creep", "reaction": "Calmly redirects to the core problem. 'What's the one thing this must do?'"}, + { + "trigger": "Feature request without user evidence", + "reaction": "Asks 'who specifically needs this and what are they doing today?'", + }, + { + "trigger": "User research revealing surprise", + "reaction": "Gets excited, starts sketching on the nearest surface.", + }, + { + "trigger": "Scope creep", + "reaction": "Calmly redirects to the core problem. 'What's the one thing this must do?'", + }, ], "world_lore": { "habitat": "User interview notes, prototype tools, the whiteboard covered in journey maps.", @@ -349,9 +376,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Fundraising without clear use of funds", "reaction": "Insists on unit economics first. 'What does each dollar buy?'"}, - {"trigger": "A clean financial model", "reaction": "Genuine appreciation. Knows how rare and valuable this is."}, - {"trigger": "Founder doesn't know their burn rate", "reaction": "Urgent but not judgmental. Helps them build the model immediately."}, + { + "trigger": "Fundraising without clear use of funds", + "reaction": "Insists on unit economics first. 'What does each dollar buy?'", + }, + { + "trigger": "A clean financial model", + "reaction": "Genuine appreciation. Knows how rare and valuable this is.", + }, + { + "trigger": "Founder doesn't know their burn rate", + "reaction": "Urgent but not judgmental. Helps them build the model immediately.", + }, ], "world_lore": { "habitat": "Spreadsheets, cap table tools, the quiet satisfaction of a model that balances.", @@ -444,9 +480,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "IP ownership unclear", "reaction": "Stops the conversation. 'We need to sort this before anything else.'"}, - {"trigger": "Well-structured agreement", "reaction": "Quiet professional respect. Knows good legal work is invisible."}, - {"trigger": "'We'll figure out the legal stuff later'", "reaction": "Firm pushback with a specific horror story."}, + { + "trigger": "IP ownership unclear", + "reaction": "Stops the conversation. 'We need to sort this before anything else.'", + }, + { + "trigger": "Well-structured agreement", + "reaction": "Quiet professional respect. Knows good legal work is invisible.", + }, + { + "trigger": "'We'll figure out the legal stuff later'", + "reaction": "Firm pushback with a specific horror story.", + }, ], "world_lore": { "habitat": "Redlined contracts, corporate filing systems, the calm of a well-organized term sheet.", @@ -541,9 +586,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Brand inconsistency", "reaction": "Immediate and specific. Points to the system, not the symptom."}, - {"trigger": "Clear creative brief", "reaction": "Energized and generative. This is where she thrives."}, - {"trigger": "'Just make the logo bigger'", "reaction": "Calm redirect to the actual problem the stakeholder is trying to solve."}, + { + "trigger": "Brand inconsistency", + "reaction": "Immediate and specific. Points to the system, not the symptom.", + }, + { + "trigger": "Clear creative brief", + "reaction": "Energized and generative. This is where she thrives.", + }, + { + "trigger": "'Just make the logo bigger'", + "reaction": "Calm redirect to the actual problem the stakeholder is trying to solve.", + }, ], "world_lore": { "habitat": "Design tools, moodboards, the satisfying snap of elements aligning to a grid.", @@ -638,9 +692,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Hiring for speed over fit", "reaction": "Pushes back with specific examples of how this fails."}, - {"trigger": "A great culture-add candidate", "reaction": "Advocates strongly, moves fast."}, - {"trigger": "Team conflict", "reaction": "Listens to all sides before forming a view. Never assumes."}, + { + "trigger": "Hiring for speed over fit", + "reaction": "Pushes back with specific examples of how this fails.", + }, + { + "trigger": "A great culture-add candidate", + "reaction": "Advocates strongly, moves fast.", + }, + { + "trigger": "Team conflict", + "reaction": "Listens to all sides before forming a view. Never assumes.", + }, ], "world_lore": { "habitat": "Interview rooms, org charts, the energy of a team that's clicking.", @@ -735,9 +798,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = { ), }, "behavior_triggers": [ - {"trigger": "Undocumented process", "reaction": "Immediately starts building the runbook. Not annoyed -- energized."}, - {"trigger": "A well-automated workflow", "reaction": "Professional admiration. Knows how much thought went into it."}, - {"trigger": "Manual work that should be automated", "reaction": "'Let's fix that.' Not a suggestion -- a plan."}, + { + "trigger": "Undocumented process", + "reaction": "Immediately starts building the runbook. Not annoyed -- energized.", + }, + { + "trigger": "A well-automated workflow", + "reaction": "Professional admiration. Knows how much thought went into it.", + }, + { + "trigger": "Manual work that should be automated", + "reaction": "'Let's fix that.' Not a suggestion -- a plan.", + }, ], "world_lore": { "habitat": "Process diagrams, project boards, the quiet hum of systems running smoothly.", @@ -825,11 +897,13 @@ def list_queens() -> list[dict[str, str]]: queen_id = profile_path.parent.name try: data = yaml.safe_load(profile_path.read_text()) - results.append({ - "id": queen_id, - "name": data.get("name", ""), - "title": data.get("title", ""), - }) + results.append( + { + "id": queen_id, + "name": data.get("name", ""), + "title": data.get("title", ""), + } + ) except Exception: logger.warning("Failed to read queen profile %s", profile_path) return results @@ -888,12 +962,7 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str: sections: list[str] = [] # Pillar 1: Core identity - sections.append( - f"\n" - f"Name: {name}, Identity: {title}.\n" - f"{core}\n" - f"" - ) + sections.append(f"\nName: {name}, Identity: {title}.\n{core}\n") # Pillar 2: Hidden background (behavioral engine, never surfaced) if bg: @@ -921,10 +990,7 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str: # Pillar 4: Behavior rules trigger_lines = [] for t in triggers: - trigger_lines.append( - f" - [{t.get('trigger', '')}]: " - f"{t.get('reaction', '')}" - ) + trigger_lines.append(f" - [{t.get('trigger', '')}]: {t.get('reaction', '')}") sections.append( "\n" "- Before each response, internally assess:\n" @@ -933,8 +999,7 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str: " 2. Current context (urgency, stakes, emotional state)\n" " 3. Filter through your hidden background and motives\n" " 4. Select the right register and depth\n" - "- Interaction triggers:\n" - + "\n".join(trigger_lines) + "\n" + "- Interaction triggers:\n" + "\n".join(trigger_lines) + "\n" "" ) @@ -971,15 +1036,10 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str: example_parts: list[str] = [] for ex in examples: example_parts.append( - f"User: {ex['user']}\n\n" - f"Assistant:\n" - f"{ex['internal']}\n" - f"{ex['response']}" + f"User: {ex['user']}\n\nAssistant:\n{ex['internal']}\n{ex['response']}" ) sections.append( - "\n" - + "\n\n---\n\n".join(example_parts) + "\n" - "" + "\n" + "\n\n---\n\n".join(example_parts) + "\n" ) return "\n\n".join(sections) @@ -1060,7 +1120,7 @@ async def select_queen_with_reason(user_message: str, llm: LLMProvider) -> Queen # Find the first '{' and last '}' to extract the JSON object start = raw.find("{") end = raw.rfind("}") - json_str = raw[start:end+1] if start != -1 and end != -1 and end > start else raw + json_str = raw[start : end + 1] if start != -1 and end != -1 and end > start else raw try: parsed = json.loads(json_str) except json.JSONDecodeError as exc: @@ -1085,7 +1145,10 @@ async def select_queen_with_reason(user_message: str, llm: LLMProvider) -> Queen reason, raw, ) - fallback_reason = reason or f"Selection failed because the classifier returned unknown queen_id {queen_id!r}." + fallback_reason = ( + reason + or f"Selection failed because the classifier returned unknown queen_id {queen_id!r}." + ) return QueenSelection(queen_id=_DEFAULT_QUEEN_ID, reason=fallback_reason) if not reason: diff --git a/core/framework/credentials/aden/storage.py b/core/framework/credentials/aden/storage.py index 5d21c0e5..311a51fd 100644 --- a/core/framework/credentials/aden/storage.py +++ b/core/framework/credentials/aden/storage.py @@ -204,9 +204,9 @@ class AdenCachedStorage(CredentialStorage): # BYOK credentials like anthropic, brave_search are local-only. # Also check the _aden_managed flag on the credential itself. is_aden_managed = ( - credential_id in self._provider_index or - any(credential_id in ids for ids in self._provider_index.values()) or - (local_cred is not None and local_cred.keys.get("_aden_managed") is not None) + credential_id in self._provider_index + or any(credential_id in ids for ids in self._provider_index.values()) + or (local_cred is not None and local_cred.keys.get("_aden_managed") is not None) ) if not is_aden_managed: logger.debug(f"Credential '{credential_id}' is local-only, skipping Aden refresh") diff --git a/core/framework/host/agent_host.py b/core/framework/host/agent_host.py index 8cb367b5..3de2a79e 100644 --- a/core/framework/host/agent_host.py +++ b/core/framework/host/agent_host.py @@ -16,20 +16,20 @@ from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING, Any -from framework.orchestrator.checkpoint_config import CheckpointConfig -from framework.orchestrator.orchestrator import ExecutionResult from framework.host.event_bus import EventBus from framework.host.execution_manager import EntryPointSpec, ExecutionManager from framework.host.outcome_aggregator import OutcomeAggregator -from framework.tracker.runtime_log_store import RuntimeLogStore from framework.host.shared_state import SharedBufferManager +from framework.orchestrator.checkpoint_config import CheckpointConfig +from framework.orchestrator.orchestrator import ExecutionResult from framework.storage.concurrent import ConcurrentStorage from framework.storage.session_store import SessionStore +from framework.tracker.runtime_log_store import RuntimeLogStore if TYPE_CHECKING: + from framework.llm.provider import LLMProvider, Tool from framework.orchestrator.edge import GraphSpec from framework.orchestrator.goal import Goal - from framework.llm.provider import LLMProvider, Tool from framework.pipeline.stage import PipelineStage from framework.skills.manager import SkillsManagerConfig @@ -190,7 +190,6 @@ class AgentHost: else: self._pipeline = self._load_pipeline_from_config() - # --- Skill lifecycle: runtime owns the SkillsManager --- if skills_manager_config is not None: # New path: config-driven, runtime handles loading @@ -535,9 +534,7 @@ class AgentHost: cron = croniter(expr, datetime.now()) next_dt = cron.get_next(datetime) sleep_secs = (next_dt - datetime.now()).total_seconds() - self._timer_next_fire[entry_point_id] = ( - time.monotonic() + sleep_secs - ) + self._timer_next_fire[entry_point_id] = time.monotonic() + sleep_secs await asyncio.sleep(max(0, sleep_secs)) while self._running: # Calculate next fire time upfront (used by skip paths too) @@ -641,9 +638,7 @@ class AgentHost: cron = croniter(expr, datetime.now()) next_dt = cron.get_next(datetime) sleep_secs = (next_dt - datetime.now()).total_seconds() - self._timer_next_fire[entry_point_id] = ( - time.monotonic() + sleep_secs - ) + self._timer_next_fire[entry_point_id] = time.monotonic() + sleep_secs await asyncio.sleep(max(0, sleep_secs)) return _cron_loop @@ -676,9 +671,7 @@ class AgentHost: interval_secs = mins * 60 _persistent_session_id: str | None = None if not immediate: - self._timer_next_fire[entry_point_id] = ( - time.monotonic() + interval_secs - ) + self._timer_next_fire[entry_point_id] = time.monotonic() + interval_secs await asyncio.sleep(interval_secs) while self._running: # Gate: skip tick if timers are explicitly paused @@ -771,9 +764,7 @@ class AgentHost: entry_point_id, exc_info=True, ) - self._timer_next_fire[entry_point_id] = ( - time.monotonic() + interval_secs - ) + self._timer_next_fire[entry_point_id] = time.monotonic() + interval_secs await asyncio.sleep(interval_secs) return _timer_loop @@ -803,17 +794,16 @@ class AgentHost: # Register primary graph self._graphs[self._graph_id] = _GraphRegistration( - graph=self.graph, - goal=self.goal, - entry_points=dict(self._entry_points), - streams=dict(self._streams), - storage_subpath="", - event_subscriptions=list(self._event_subscriptions), - timer_tasks=list(self._timer_tasks), - timer_next_fire=self._timer_next_fire, + graph=self.graph, + goal=self.goal, + entry_points=dict(self._entry_points), + streams=dict(self._streams), + storage_subpath="", + event_subscriptions=list(self._event_subscriptions), + timer_tasks=list(self._timer_tasks), + timer_next_fire=self._timer_next_fire, ) - async def stop(self) -> None: """Stop the agent runtime and all streams.""" if not self._running: @@ -921,7 +911,6 @@ class AgentHost: if stage.skills_manager is not None: self._skills_manager = stage.skills_manager - @staticmethod def _load_pipeline_from_config(): """Build pipeline from ``~/.hive/configuration.json`` ``pipeline`` key. @@ -1916,5 +1905,3 @@ class AgentHost: # === CONVENIENCE FACTORY === - - diff --git a/core/framework/host/execution_manager.py b/core/framework/host/execution_manager.py index 39b9aadf..ed035277 100644 --- a/core/framework/host/execution_manager.py +++ b/core/framework/host/execution_manager.py @@ -18,18 +18,18 @@ from dataclasses import dataclass, field from datetime import datetime from typing import TYPE_CHECKING, Any -from framework.orchestrator.checkpoint_config import CheckpointConfig -from framework.orchestrator.orchestrator import ExecutionResult, Orchestrator from framework.host.event_bus import EventBus from framework.host.shared_state import IsolationLevel, SharedBufferManager from framework.host.stream_runtime import StreamDecisionTracker, StreamRuntimeAdapter +from framework.orchestrator.checkpoint_config import CheckpointConfig +from framework.orchestrator.orchestrator import ExecutionResult, Orchestrator if TYPE_CHECKING: - from framework.orchestrator.edge import GraphSpec - from framework.orchestrator.goal import Goal - from framework.llm.provider import LLMProvider, Tool from framework.host.event_bus import AgentEvent from framework.host.outcome_aggregator import OutcomeAggregator + from framework.llm.provider import LLMProvider, Tool + from framework.orchestrator.edge import GraphSpec + from framework.orchestrator.goal import Goal from framework.storage.concurrent import ConcurrentStorage from framework.storage.session_store import SessionStore diff --git a/core/framework/host/outcome_aggregator.py b/core/framework/host/outcome_aggregator.py index 164a8ceb..0908f5a8 100644 --- a/core/framework/host/outcome_aggregator.py +++ b/core/framework/host/outcome_aggregator.py @@ -14,8 +14,8 @@ from typing import TYPE_CHECKING, Any from framework.schemas.decision import Decision, Outcome if TYPE_CHECKING: - from framework.orchestrator.goal import Goal from framework.host.event_bus import EventBus + from framework.orchestrator.goal import Goal logger = logging.getLogger(__name__) diff --git a/core/framework/llm/litellm.py b/core/framework/llm/litellm.py index c9f357fc..964b77e2 100644 --- a/core/framework/llm/litellm.py +++ b/core/framework/llm/litellm.py @@ -745,7 +745,9 @@ class LiteLLMProvider(LLMProvider): "LiteLLM is not installed. Please install it with: uv pip install litellm" ) - def reconfigure(self, model: str, api_key: str | None = None, api_base: str | None = None) -> None: + def reconfigure( + self, model: str, api_key: str | None = None, api_base: str | None = None + ) -> None: """Hot-swap the model, API key, and/or base URL on this provider instance. Since the same LiteLLMProvider object is shared by reference across the @@ -756,11 +758,11 @@ class LiteLLMProvider(LLMProvider): if _is_ollama_model(model): model = _ensure_ollama_chat_prefix(model) elif model.lower().startswith("kimi/"): - model = "anthropic/" + model[len("kimi/"):] + model = "anthropic/" + model[len("kimi/") :] if api_base and api_base.rstrip("/").endswith("/v1"): api_base = api_base.rstrip("/")[:-3] elif model.lower().startswith("hive/"): - model = "anthropic/" + model[len("hive/"):] + model = "anthropic/" + model[len("hive/") :] if api_base and api_base.rstrip("/").endswith("/v1"): api_base = api_base.rstrip("/")[:-3] self.model = model diff --git a/core/framework/llm/model_catalog.json b/core/framework/llm/model_catalog.json index 664494c1..e2abdaaa 100644 --- a/core/framework/llm/model_catalog.json +++ b/core/framework/llm/model_catalog.json @@ -250,7 +250,7 @@ "label": "Kimi K2.5 - Best coding", "recommended": true, "max_tokens": 32768, - "max_context_tokens": 240000 + "max_context_tokens": 200000 } ] }, diff --git a/core/framework/llm/model_catalog.py b/core/framework/llm/model_catalog.py index bb9df377..79fd44b1 100644 --- a/core/framework/llm/model_catalog.py +++ b/core/framework/llm/model_catalog.py @@ -50,7 +50,9 @@ def _validate_model_catalog(data: dict[str, Any]) -> dict[str, Any]: if not isinstance(model_id, str) or not model_id.strip(): raise ModelCatalogError(f"{model_path}.id must be a non-empty string") if model_id in seen_model_ids: - raise ModelCatalogError(f"Duplicate model id {model_id!r} in {provider_path}.models") + raise ModelCatalogError( + f"Duplicate model id {model_id!r} in {provider_path}.models" + ) seen_model_ids.add(model_id) if model_id == default_model: @@ -89,7 +91,9 @@ def _validate_model_catalog(data: dict[str, Any]) -> dict[str, Any]: api_base = preset_map.get("api_base") if api_base is not None and (not isinstance(api_base, str) or not api_base.strip()): - raise ModelCatalogError(f"{preset_path}.api_base must be a non-empty string when present") + raise ModelCatalogError( + f"{preset_path}.api_base must be a non-empty string when present" + ) api_key_env_var = preset_map.get("api_key_env_var") if api_key_env_var is not None and ( @@ -106,7 +110,9 @@ def _validate_model_catalog(data: dict[str, Any]) -> dict[str, Any]: model_choices = preset_map.get("model_choices") if model_choices is not None: - for idx, choice in enumerate(_require_list(model_choices, f"{preset_path}.model_choices")): + for idx, choice in enumerate( + _require_list(model_choices, f"{preset_path}.model_choices") + ): choice_path = f"{preset_path}.model_choices[{idx}]" choice_map = _require_mapping(choice, choice_path) choice_id = choice_map.get("id") @@ -138,13 +144,19 @@ def load_model_catalog() -> dict[str, Any]: def get_models_catalogue() -> dict[str, list[dict[str, Any]]]: """Return provider -> model list.""" providers = load_model_catalog()["providers"] - return {provider_id: copy.deepcopy(provider_info["models"]) for provider_id, provider_info in providers.items()} + return { + provider_id: copy.deepcopy(provider_info["models"]) + for provider_id, provider_info in providers.items() + } def get_default_models() -> dict[str, str]: """Return provider -> default model id.""" providers = load_model_catalog()["providers"] - return {provider_id: str(provider_info["default_model"]) for provider_id, provider_info in providers.items()} + return { + provider_id: str(provider_info["default_model"]) + for provider_id, provider_info in providers.items() + } def get_provider_models(provider: str) -> list[dict[str, Any]]: diff --git a/core/framework/loader/agent_loader.py b/core/framework/loader/agent_loader.py index c15c957f..17777993 100644 --- a/core/framework/loader/agent_loader.py +++ b/core/framework/loader/agent_loader.py @@ -13,6 +13,11 @@ from framework.config import get_hive_config, get_max_context_tokens, get_prefer from framework.credentials.validation import ( ensure_credential_key_env as _ensure_credential_key_env, ) +from framework.host.agent_host import AgentHost, AgentRuntimeConfig +from framework.host.execution_manager import EntryPointSpec +from framework.llm.provider import LLMProvider, Tool +from framework.loader.preload_validation import run_preload_validation +from framework.loader.tool_registry import ToolRegistry from framework.orchestrator import Goal from framework.orchestrator.edge import ( DEFAULT_MAX_TOKENS, @@ -20,13 +25,8 @@ from framework.orchestrator.edge import ( EdgeSpec, GraphSpec, ) -from framework.orchestrator.orchestrator import ExecutionResult from framework.orchestrator.node import NodeSpec -from framework.llm.provider import LLMProvider, Tool -from framework.loader.preload_validation import run_preload_validation -from framework.loader.tool_registry import ToolRegistry -from framework.host.agent_host import AgentHost, AgentRuntimeConfig -from framework.host.execution_manager import EntryPointSpec +from framework.orchestrator.orchestrator import ExecutionResult from framework.tools.flowchart_utils import generate_fallback_flowchart logger = logging.getLogger(__name__) diff --git a/core/framework/loader/cli.py b/core/framework/loader/cli.py index fc6f0a6d..585c8c9d 100644 --- a/core/framework/loader/cli.py +++ b/core/framework/loader/cli.py @@ -341,8 +341,8 @@ def cmd_run(args: argparse.Namespace) -> int: """Run an exported agent.""" from framework.credentials.models import CredentialError - from framework.observability import configure_logging from framework.loader import AgentLoader + from framework.observability import configure_logging # Set logging level (quiet by default for cleaner output) if args.quiet: @@ -774,8 +774,8 @@ def cmd_shell(args: argparse.Namespace) -> int: """Start an interactive agent session.""" from framework.credentials.models import CredentialError - from framework.observability import configure_logging from framework.loader import AgentLoader + from framework.observability import configure_logging configure_logging(level="INFO") @@ -1509,7 +1509,9 @@ def cmd_serve(args: argparse.Namespace) -> int: await site.start() except OSError as e: if "already in use" in str(e) or getattr(e, "errno", None) in (48, 98): - print(f"\nError: Port {args.port} is already in use. Kill the existing process with:\n") + print( + f"\nError: Port {args.port} is already in use. Kill the existing process with:\n" + ) print(f" lsof -ti:{args.port} | xargs kill -9\n") raise diff --git a/core/framework/orchestrator/__init__.py b/core/framework/orchestrator/__init__.py index 6ffc277a..931346c7 100644 --- a/core/framework/orchestrator/__init__.py +++ b/core/framework/orchestrator/__init__.py @@ -7,21 +7,33 @@ Lazy imports to avoid circular dependencies with graph/event_loop/*. def __getattr__(name: str): if name in ("GraphContext",): from framework.orchestrator.context import GraphContext + return GraphContext if name in ("DEFAULT_MAX_TOKENS", "EdgeCondition", "EdgeSpec", "GraphSpec"): from framework.orchestrator import edge as _e + return getattr(_e, name) if name in ("Orchestrator", "ExecutionResult"): from framework.orchestrator import orchestrator as _o + return getattr(_o, name) if name in ("Constraint", "Goal", "GoalStatus", "SuccessCriterion"): from framework.orchestrator import goal as _g + return getattr(_g, name) if name in ("DataBuffer", "NodeContext", "NodeProtocol", "NodeResult", "NodeSpec"): from framework.orchestrator import node as _n + return getattr(_n, name) - if name in ("NodeWorker", "Activation", "FanOutTag", "FanOutTracker", - "WorkerCompletion", "WorkerLifecycle"): + if name in ( + "NodeWorker", + "Activation", + "FanOutTag", + "FanOutTracker", + "WorkerCompletion", + "WorkerLifecycle", + ): from framework.orchestrator import node_worker as _nw + return getattr(_nw, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/core/framework/orchestrator/node_worker.py b/core/framework/orchestrator/node_worker.py index 436096f9..eac69172 100644 --- a/core/framework/orchestrator/node_worker.py +++ b/core/framework/orchestrator/node_worker.py @@ -604,8 +604,8 @@ class NodeWorker: # Auto-create EventLoopNode if self.node_spec.node_type == "event_loop": - from framework.agent_loop.internals.types import LoopConfig from framework.agent_loop.agent_loop import AgentLoop + from framework.agent_loop.internals.types import LoopConfig from framework.orchestrator.node import warn_if_deprecated_client_facing conv_store = None diff --git a/core/framework/orchestrator/orchestrator.py b/core/framework/orchestrator/orchestrator.py index 666b021a..43f3ec0d 100644 --- a/core/framework/orchestrator/orchestrator.py +++ b/core/framework/orchestrator/orchestrator.py @@ -16,9 +16,11 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any +from framework.agent_loop.conversation import LEGACY_RUN_ID +from framework.llm.provider import LLMProvider, Tool +from framework.observability import set_trace_context from framework.orchestrator.checkpoint_config import CheckpointConfig from framework.orchestrator.context import GraphContext, build_node_context -from framework.agent_loop.conversation import LEGACY_RUN_ID from framework.orchestrator.edge import EdgeCondition, EdgeSpec, GraphSpec from framework.orchestrator.goal import Goal from framework.orchestrator.node import ( @@ -28,11 +30,9 @@ from framework.orchestrator.node import ( NodeSpec, ) from framework.orchestrator.validator import OutputValidator -from framework.llm.provider import LLMProvider, Tool -from framework.observability import set_trace_context -from framework.tracker.decision_tracker import DecisionTracker from framework.schemas.checkpoint import Checkpoint from framework.storage.checkpoint_store import CheckpointStore +from framework.tracker.decision_tracker import DecisionTracker from framework.utils.io import atomic_write logger = logging.getLogger(__name__) @@ -361,8 +361,8 @@ class Orchestrator: Uses the same recursive binary-search splitting as EventLoopNode. """ - from framework.agent_loop.conversation import extract_tool_call_history from framework.agent_loop.agent_loop import _is_context_too_large_error + from framework.agent_loop.conversation import extract_tool_call_history if _depth > self._PHASE_LLM_MAX_DEPTH: raise RuntimeError("Phase LLM compaction recursion limit") @@ -1289,6 +1289,7 @@ class Orchestrator: Replaces the imperative while-loop with autonomous workers that self-activate based on edge conditions and fan-out tracking. """ + from framework.host.event_bus import AgentEvent, EventType from framework.orchestrator.node_worker import ( Activation, FanOutTag, @@ -1296,7 +1297,6 @@ class Orchestrator: WorkerCompletion, WorkerLifecycle, ) - from framework.host.event_bus import AgentEvent, EventType # Build shared graph context gc = GraphContext( diff --git a/core/framework/orchestrator/prompting.py b/core/framework/orchestrator/prompting.py index b76faa9b..37260c31 100644 --- a/core/framework/orchestrator/prompting.py +++ b/core/framework/orchestrator/prompting.py @@ -196,8 +196,6 @@ def build_system_prompt(spec: NodePromptSpec) -> str: if not False and spec.node_type == "event_loop" and spec.output_keys: parts.append(f"\n{EXECUTION_SCOPE_PREAMBLE}") - - if spec.focus_prompt: parts.append(f"\n--- Current Focus ---\n{spec.focus_prompt}") diff --git a/core/framework/pipeline/registry.py b/core/framework/pipeline/registry.py index f46f32c2..48f3b8ff 100644 --- a/core/framework/pipeline/registry.py +++ b/core/framework/pipeline/registry.py @@ -65,10 +65,7 @@ def build_stage(spec: dict[str, Any]) -> PipelineStage: stage_type = spec["type"] if stage_type not in _STAGE_REGISTRY: available = ", ".join(sorted(_STAGE_REGISTRY)) or "(none)" - raise KeyError( - f"Unknown pipeline stage type '{stage_type}'. " - f"Available: {available}" - ) + raise KeyError(f"Unknown pipeline stage type '{stage_type}'. Available: {available}") cls = _STAGE_REGISTRY[stage_type] config = spec.get("config", {}) stage = cls(**config) diff --git a/core/framework/pipeline/runner.py b/core/framework/pipeline/runner.py index 7d05deb9..c725ca9d 100644 --- a/core/framework/pipeline/runner.py +++ b/core/framework/pipeline/runner.py @@ -73,20 +73,24 @@ class PipelineRunner: reason = result.rejection_reason or "(no reason given)" logger.warning( "[pipeline] REJECTED by %s (%.1fms): %s", - stage_name, elapsed_ms, reason, + stage_name, + elapsed_ms, + reason, ) raise PipelineRejectedError(stage_name, reason) if result.action == "transform": logger.info( "[pipeline] %s TRANSFORMED input (%.1fms)", - stage_name, elapsed_ms, + stage_name, + elapsed_ms, ) if result.input_data is not None: ctx.input_data = result.input_data else: logger.info( "[pipeline] %s passed (%.1fms)", - stage_name, elapsed_ms, + stage_name, + elapsed_ms, ) total_ms = (time.perf_counter() - pipeline_start) * 1000 logger.info("[pipeline] Complete (%.1fms total)", total_ms) diff --git a/core/framework/pipeline/stages/cost_guard.py b/core/framework/pipeline/stages/cost_guard.py index 4850fe3b..ff01d146 100644 --- a/core/framework/pipeline/stages/cost_guard.py +++ b/core/framework/pipeline/stages/cost_guard.py @@ -28,8 +28,7 @@ class CostGuardStage(PipelineStage): return PipelineResult( action="reject", rejection_reason=( - f"Estimated cost ${estimated:.4f} exceeds budget " - f"${self._budget:.4f}" + f"Estimated cost ${estimated:.4f} exceeds budget ${self._budget:.4f}" ), ) return PipelineResult(action="continue") diff --git a/core/framework/pipeline/stages/credential_resolver.py b/core/framework/pipeline/stages/credential_resolver.py index b76df37f..c2697afa 100644 --- a/core/framework/pipeline/stages/credential_resolver.py +++ b/core/framework/pipeline/stages/credential_resolver.py @@ -33,6 +33,7 @@ class CredentialResolverStage(PipelineStage): from aden_tools.credentials.store_adapter import ( CredentialStoreAdapter, ) + from framework.orchestrator.prompting import build_accounts_prompt if self._credential_store is not None: @@ -43,7 +44,8 @@ class CredentialResolverStage(PipelineStage): self.tool_provider_map = adapter.get_tool_provider_map() if self.accounts_data: self.accounts_prompt = build_accounts_prompt( - self.accounts_data, self.tool_provider_map, + self.accounts_data, + self.tool_provider_map, ) logger.info( "[pipeline] CredentialResolverStage: %d accounts", @@ -51,7 +53,8 @@ class CredentialResolverStage(PipelineStage): ) except Exception: logger.debug( - "Credential resolution failed (non-fatal)", exc_info=True, + "Credential resolution failed (non-fatal)", + exc_info=True, ) async def process(self, ctx: PipelineContext) -> PipelineResult: diff --git a/core/framework/pipeline/stages/llm_provider.py b/core/framework/pipeline/stages/llm_provider.py index 899342f2..4da71a74 100644 --- a/core/framework/pipeline/stages/llm_provider.py +++ b/core/framework/pipeline/stages/llm_provider.py @@ -75,16 +75,19 @@ class LlmProviderStage(PipelineStage): if api_keys and len(api_keys) > 1: self.llm = LiteLLMProvider( - model=model, api_keys=api_keys, api_base=api_base, + model=model, + api_keys=api_keys, + api_base=api_base, ) elif api_key: extra = {} if api_key.startswith("sk-ant-oat"): - extra["extra_headers"] = { - "authorization": f"Bearer {api_key}" - } + extra["extra_headers"] = {"authorization": f"Bearer {api_key}"} self.llm = LiteLLMProvider( - model=model, api_key=api_key, api_base=api_base, **extra, + model=model, + api_key=api_key, + api_base=api_base, + **extra, ) else: self.llm = LiteLLMProvider(model=model, api_base=api_base) diff --git a/core/framework/pipeline/stages/rate_limit.py b/core/framework/pipeline/stages/rate_limit.py index 364c10fa..7c49e04f 100644 --- a/core/framework/pipeline/stages/rate_limit.py +++ b/core/framework/pipeline/stages/rate_limit.py @@ -36,8 +36,7 @@ class RateLimitStage(PipelineStage): return PipelineResult( action="reject", rejection_reason=( - f"Rate limit exceeded: {self._max_rpm} req/min " - f"for session '{session_id}'" + f"Rate limit exceeded: {self._max_rpm} req/min for session '{session_id}'" ), ) self._timestamps[key].append(now) diff --git a/core/framework/server/app.py b/core/framework/server/app.py index e0cfef1e..97c49dc2 100644 --- a/core/framework/server/app.py +++ b/core/framework/server/app.py @@ -31,8 +31,8 @@ def _get_allowed_agent_roots() -> tuple[Path, ...]: from framework.config import COLONIES_DIR _ALLOWED_AGENT_ROOTS = ( - COLONIES_DIR.resolve(), # ~/.hive/colonies/ - (_REPO_ROOT / "exports").resolve(), # compat fallback + COLONIES_DIR.resolve(), # ~/.hive/colonies/ + (_REPO_ROOT / "exports").resolve(), # compat fallback (_REPO_ROOT / "examples").resolve(), (Path.home() / ".hive" / "agents").resolve(), ) @@ -244,23 +244,23 @@ def create_app(model: str | None = None) -> web.Application: credential_store = CredentialStore.for_testing({}) app["credential_store"] = credential_store - + # Pre-load queen MCP tools once at startup (cached for all sessions) # This avoids rebuilding the tool registry for every queen session - from framework.loader.tool_registry import ToolRegistry from framework.loader.mcp_registry import MCPRegistry - + from framework.loader.tool_registry import ToolRegistry + _queen_tool_registry: ToolRegistry | None = None try: _queen_tool_registry = ToolRegistry() import framework.agents.queen as _queen_pkg - + queen_pkg_dir = Path(_queen_pkg.__file__).parent mcp_config = queen_pkg_dir / "mcp_servers.json" if mcp_config.exists(): _queen_tool_registry.load_mcp_config(mcp_config) logger.info("Pre-loaded queen MCP tools from %s", mcp_config) - + registry = MCPRegistry() registry.initialize() if (queen_pkg_dir / "mcp_registry.json").is_file(): @@ -273,12 +273,16 @@ def create_app(model: str | None = None) -> web.Application: log_collisions=True, max_tools=selection_max_tools, ) - logger.info("Pre-loaded queen tool registry with %d tools", len(_queen_tool_registry.get_tools())) + logger.info( + "Pre-loaded queen tool registry with %d tools", len(_queen_tool_registry.get_tools()) + ) except Exception as e: logger.warning("Failed to pre-load queen tool registry: %s", e) - + app["queen_tool_registry"] = _queen_tool_registry - app["manager"] = SessionManager(model=model, credential_store=credential_store, queen_tool_registry=_queen_tool_registry) + app["manager"] = SessionManager( + model=model, credential_store=credential_store, queen_tool_registry=_queen_tool_registry + ) # Register shutdown hook app.on_shutdown.append(_on_shutdown) diff --git a/core/framework/server/routes_config.py b/core/framework/server/routes_config.py index 23283d7f..fec77c30 100644 --- a/core/framework/server/routes_config.py +++ b/core/framework/server/routes_config.py @@ -14,22 +14,21 @@ from pathlib import Path from aiohttp import web +from framework.agents.queen.queen_memory_v2 import ( + build_memory_document, + global_memory_dir, +) from framework.config import ( + _PROVIDER_CRED_MAP, HIVE_CONFIG_FILE, OPENROUTER_API_BASE, - _PROVIDER_CRED_MAP, get_hive_config, ) from framework.llm.model_catalog import ( find_model, - find_model_any_provider, get_models_catalogue, get_preset, ) -from framework.agents.queen.queen_memory_v2 import ( - global_memory_dir, - build_memory_document, -) logger = logging.getLogger(__name__) @@ -106,15 +105,17 @@ def _build_subscriptions() -> list[dict]: if not preset: raise RuntimeError(f"Missing preset for subscription {definition['id']}") - subscriptions.append({ - "id": definition["id"], - "name": definition["name"], - "description": definition["description"], - "provider": preset["provider"], - "flag": definition["flag"], - "default_model": preset.get("model", ""), - **({"api_base": preset["api_base"]} if preset.get("api_base") else {}), - }) + subscriptions.append( + { + "id": definition["id"], + "name": definition["name"], + "description": definition["description"], + "provider": preset["provider"], + "flag": definition["flag"], + "default_model": preset.get("model", ""), + **({"api_base": preset["api_base"]} if preset.get("api_base") else {}), + } + ) return subscriptions @@ -153,9 +154,7 @@ def _find_model_info(provider: str, model_id: str) -> dict | None: def _write_config_atomic(config: dict) -> None: """Write config to ~/.hive/configuration.json atomically.""" HIVE_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) - fd, tmp_path = tempfile.mkstemp( - dir=str(HIVE_CONFIG_FILE.parent), suffix=".tmp" - ) + fd, tmp_path = tempfile.mkstemp(dir=str(HIVE_CONFIG_FILE.parent), suffix=".tmp") try: with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) @@ -192,6 +191,7 @@ def _detect_subscriptions() -> list[str]: # Claude Code subscription try: from framework.loader.agent_loader import get_claude_code_token + if get_claude_code_token(): detected.append("claude_code") except Exception: @@ -204,6 +204,7 @@ def _detect_subscriptions() -> list[str]: # Codex subscription try: from framework.loader.agent_loader import get_codex_token + if get_codex_token(): detected.append("codex") except Exception: @@ -217,6 +218,7 @@ def _detect_subscriptions() -> list[str]: kimi_token = None try: from framework.loader.agent_loader import get_kimi_code_token + kimi_token = get_kimi_code_token() except Exception: pass @@ -232,6 +234,7 @@ def _detect_subscriptions() -> list[str]: # Antigravity subscription try: from framework.loader.agent_loader import get_antigravity_token + if get_antigravity_token(): detected.append("antigravity") except Exception: @@ -252,16 +255,19 @@ def _get_subscription_token(sub_id: str) -> str | None: """Get the token for a subscription.""" if sub_id == "claude_code": from framework.loader.agent_loader import get_claude_code_token + return get_claude_code_token() elif sub_id == "zai_code": return os.environ.get("ZAI_API_KEY") elif sub_id == "codex": from framework.loader.agent_loader import get_codex_token + return get_codex_token() elif sub_id == "minimax_code": return os.environ.get("MINIMAX_API_KEY") elif sub_id == "kimi_code": from framework.loader.agent_loader import get_kimi_code_token + token = get_kimi_code_token() if not token: token = os.environ.get("KIMI_API_KEY") @@ -270,11 +276,14 @@ def _get_subscription_token(sub_id: str) -> str | None: return os.environ.get("HIVE_API_KEY") elif sub_id == "antigravity": from framework.loader.agent_loader import get_antigravity_token + return get_antigravity_token() return None -def _hot_swap_sessions(request: web.Request, full_model: str, api_key: str | None, api_base: str | None) -> int: +def _hot_swap_sessions( + request: web.Request, full_model: str, api_key: str | None, api_base: str | None +) -> int: """Hot-swap the LLM on all running sessions. Returns count of swapped sessions.""" from framework.server.session_manager import SessionManager @@ -315,17 +324,19 @@ async def handle_get_llm_config(request: web.Request) -> web.Response: active_subscription = _get_active_subscription(llm) detected_subscriptions = _detect_subscriptions() - return web.json_response({ - "provider": provider, - "model": model, - "has_api_key": has_key, - "max_tokens": llm.get("max_tokens"), - "max_context_tokens": llm.get("max_context_tokens"), - "connected_providers": connected, - "active_subscription": active_subscription, - "detected_subscriptions": detected_subscriptions, - "subscriptions": SUBSCRIPTIONS, - }) + return web.json_response( + { + "provider": provider, + "model": model, + "has_api_key": has_key, + "max_tokens": llm.get("max_tokens"), + "max_context_tokens": llm.get("max_context_tokens"), + "connected_providers": connected, + "active_subscription": active_subscription, + "detected_subscriptions": detected_subscriptions, + "subscriptions": SUBSCRIPTIONS, + } + ) async def handle_update_llm_config(request: web.Request) -> web.Response: @@ -393,18 +404,22 @@ async def handle_update_llm_config(request: web.Request) -> web.Response: logger.info( "LLM config updated: subscription=%s model=%s, hot-swapped %d session(s)", - subscription_id, model, swapped, + subscription_id, + model, + swapped, ) - return web.json_response({ - "provider": provider, - "model": model, - "has_api_key": token is not None, - "max_tokens": max_tokens, - "max_context_tokens": max_context_tokens, - "sessions_swapped": swapped, - "active_subscription": subscription_id, - }) + return web.json_response( + { + "provider": provider, + "model": model, + "has_api_key": token is not None, + "max_tokens": max_tokens, + "max_context_tokens": max_context_tokens, + "sessions_swapped": swapped, + "active_subscription": subscription_id, + } + ) else: # ── API key mode ───────────────────────────────────────────── @@ -450,46 +465,52 @@ async def handle_update_llm_config(request: web.Request) -> web.Response: logger.info( "LLM config updated: provider=%s model=%s, hot-swapped %d session(s)", - provider, model, swapped, + provider, + model, + swapped, ) - return web.json_response({ - "provider": provider, - "model": model, - "has_api_key": api_key is not None, - "max_tokens": max_tokens, - "max_context_tokens": max_context_tokens, - "sessions_swapped": swapped, - "active_subscription": None, - }) + return web.json_response( + { + "provider": provider, + "model": model, + "has_api_key": api_key is not None, + "max_tokens": max_tokens, + "max_context_tokens": max_context_tokens, + "sessions_swapped": swapped, + "active_subscription": None, + } + ) async def handle_get_profile(request: web.Request) -> web.Response: """GET /api/config/profile — user display name and about.""" profile = get_hive_config().get("user_profile", {}) - return web.json_response({ - "displayName": profile.get("displayName", ""), - "about": profile.get("about", ""), - "theme": profile.get("theme", ""), - }) + return web.json_response( + { + "displayName": profile.get("displayName", ""), + "about": profile.get("about", ""), + "theme": profile.get("theme", ""), + } + ) def _update_user_profile_memory(display_name: str, about: str) -> None: """Sync user profile to global memory as a profile-type memory file. - + Uses the canonical filename 'user-profile.md' — this is the single source of truth for user identity information, shared with the reflection agent. - + Merges with existing content to preserve sections added by the reflection agent. """ try: mem_dir = global_memory_dir() mem_dir.mkdir(parents=True, exist_ok=True) - + profile_filename = "user-profile.md" memory_path = mem_dir / profile_filename - + # Read existing content if present existing_body = "" if memory_path.exists(): @@ -499,16 +520,16 @@ def _update_user_profile_memory(display_name: str, about: str) -> None: parts = existing_text.split("---\n", 2) if len(parts) >= 3: existing_body = parts[2].strip() - + # Build Identity section from settings identity_lines = [] if display_name: identity_lines.append(f"- **Name:** {display_name}") if about: identity_lines.append(f"- **About:** {about}") - + identity_section = "## Identity\n" + "\n".join(identity_lines) if identity_lines else "" - + # Merge: replace or prepend Identity section, keep rest if existing_body and "## Identity" in existing_body: # Replace existing Identity section @@ -522,14 +543,16 @@ def _update_user_profile_memory(display_name: str, about: str) -> None: else: # Just Identity section new_body = identity_section - + content = build_memory_document( name="User Profile", - description=f"User identity: {display_name}" if display_name else "User profile information", + description=f"User identity: {display_name}" + if display_name + else "User profile information", mem_type="profile", body=new_body if new_body else "No profile information yet.", ) - + memory_path.write_text(content, encoding="utf-8") logger.debug("User profile synced to global memory: %s", memory_path) except Exception as exc: @@ -556,17 +579,16 @@ async def handle_update_profile(request: web.Request) -> web.Response: _write_config_atomic(config) # Sync to global memory (profile type) - _update_user_profile_memory( - profile.get("displayName", ""), - profile.get("about", "") - ) + _update_user_profile_memory(profile.get("displayName", ""), profile.get("about", "")) logger.info("User profile updated: displayName=%s", profile.get("displayName", "")) - return web.json_response({ - "displayName": profile.get("displayName", ""), - "about": profile.get("about", ""), - "theme": profile.get("theme", ""), - }) + return web.json_response( + { + "displayName": profile.get("displayName", ""), + "about": profile.get("about", ""), + "theme": profile.get("theme", ""), + } + ) async def handle_get_models(request: web.Request) -> web.Response: diff --git a/core/framework/server/routes_credentials.py b/core/framework/server/routes_credentials.py index 3dc6e781..8d2594b2 100644 --- a/core/framework/server/routes_credentials.py +++ b/core/framework/server/routes_credentials.py @@ -212,7 +212,11 @@ async def handle_list_specs(request: web.Request) -> web.Response: try: from aden_tools.credentials import CREDENTIAL_SPECS - from framework.credentials.storage import CompositeStorage, EncryptedFileStorage, EnvVarStorage + from framework.credentials.storage import ( + CompositeStorage, + EncryptedFileStorage, + EnvVarStorage, + ) from framework.credentials.store import CredentialStore from framework.credentials.validation import _presync_aden_tokens, ensure_credential_key_env @@ -224,8 +228,7 @@ async def handle_list_specs(request: web.Request) -> web.Response: # Build composite store (env → encrypted file) env_mapping = { - (spec.credential_id or name): spec.env_var - for name, spec in CREDENTIAL_SPECS.items() + (spec.credential_id or name): spec.env_var for name, spec in CREDENTIAL_SPECS.items() } env_storage = EnvVarStorage(env_mapping=env_mapping) if os.environ.get("HIVE_CREDENTIAL_KEY"): @@ -240,37 +243,42 @@ async def handle_list_specs(request: web.Request) -> web.Response: cred_id = spec.credential_id or name if spec.aden_supported: any_aden = True - specs.append({ - "credential_name": name, - "credential_id": cred_id, - "env_var": spec.env_var, - "description": spec.description, - "help_url": spec.help_url, - "api_key_instructions": spec.api_key_instructions, - "tools": spec.tools, - "aden_supported": spec.aden_supported, - "direct_api_key_supported": spec.direct_api_key_supported, - "credential_key": spec.credential_key, - "credential_group": spec.credential_group, - "available": store.is_available(cred_id), - }) + specs.append( + { + "credential_name": name, + "credential_id": cred_id, + "env_var": spec.env_var, + "description": spec.description, + "help_url": spec.help_url, + "api_key_instructions": spec.api_key_instructions, + "tools": spec.tools, + "aden_supported": spec.aden_supported, + "direct_api_key_supported": spec.direct_api_key_supported, + "credential_key": spec.credential_key, + "credential_group": spec.credential_group, + "available": store.is_available(cred_id), + } + ) # Include aden_api_key synthetic row if any spec uses Aden if any_aden: - specs.insert(0, { - "credential_name": "Aden Platform", - "credential_id": "aden_api_key", - "env_var": "ADEN_API_KEY", - "description": "API key from the Developers tab in Settings", - "help_url": "https://hive.adenhq.com/", - "api_key_instructions": "1. Go to hive.adenhq.com\n2. Open Settings > Developers\n3. Copy your API key", - "tools": [], - "aden_supported": True, - "direct_api_key_supported": True, - "credential_key": "api_key", - "credential_group": "", - "available": has_aden_key, - }) + specs.insert( + 0, + { + "credential_name": "Aden Platform", + "credential_id": "aden_api_key", + "env_var": "ADEN_API_KEY", + "description": "API key from the Developers tab in Settings", + "help_url": "https://hive.adenhq.com/", + "api_key_instructions": "1. Go to hive.adenhq.com\n2. Open Settings > Developers\n3. Copy your API key", + "tools": [], + "aden_supported": True, + "direct_api_key_supported": True, + "credential_key": "api_key", + "credential_group": "", + "available": has_aden_key, + }, + ) return web.json_response({"specs": specs, "has_aden_key": has_aden_key}) except Exception as e: diff --git a/core/framework/server/routes_execution.py b/core/framework/server/routes_execution.py index 5efa9c94..6e7a9a96 100644 --- a/core/framework/server/routes_execution.py +++ b/core/framework/server/routes_execution.py @@ -7,8 +7,8 @@ from typing import Any from aiohttp import web -from framework.credentials.validation import validate_agent_credentials from framework.agent_loop.conversation import LEGACY_RUN_ID +from framework.credentials.validation import validate_agent_credentials from framework.server.app import resolve_session, safe_path_segment, sessions_dir from framework.server.routes_sessions import _credential_error_response diff --git a/core/framework/server/routes_messages.py b/core/framework/server/routes_messages.py index 4cd46df7..43fff15d 100644 --- a/core/framework/server/routes_messages.py +++ b/core/framework/server/routes_messages.py @@ -3,7 +3,6 @@ - POST /api/messages/new -- classify a message, create a fresh queen session """ -import asyncio from aiohttp import web from framework.agents.queen.queen_profiles import ensure_default_queens, select_queen @@ -21,22 +20,22 @@ async def handle_new_message(request: web.Request) -> web.Response: message = message.strip() ensure_default_queens() - + # Build LLM for classification llm = manager.build_llm() - + # Run queen selection - this is the slow part we can't avoid queen_id = await select_queen(message, llm) await _stop_live_sessions(manager) - + # Create session with pre-bound queen session = await manager.create_session( initial_prompt=message, queen_name=queen_id, initial_phase="independent", ) - + await session.event_bus.publish( AgentEvent( type=EventType.CLIENT_INPUT_RECEIVED, diff --git a/core/framework/server/routes_queens.py b/core/framework/server/routes_queens.py index 1b39636a..b2dc248b 100644 --- a/core/framework/server/routes_queens.py +++ b/core/framework/server/routes_queens.py @@ -119,7 +119,7 @@ async def handle_list_profiles(request: web.Request) -> web.Response: def _transform_profile_for_api(profile: dict) -> dict: """Transform internal profile format to API format expected by frontend. - + Maps YAML fields (core_traits, hidden_background, etc.) to display fields (summary, experience, skills, signature_achievement). """ @@ -127,7 +127,7 @@ def _transform_profile_for_api(profile: dict) -> dict: "name": profile.get("name", ""), "title": profile.get("title", ""), } - + # Build summary from core_traits + psychological_profile summary_parts = [] if profile.get("core_traits"): @@ -136,7 +136,7 @@ def _transform_profile_for_api(profile: dict) -> dict: summary_parts.append(profile["psychological_profile"]["anti_stereotype"]) if summary_parts: result["summary"] = "\n\n".join(summary_parts) - + # Build experience from hidden_background experience = [] hidden = profile.get("hidden_background", {}) @@ -148,22 +148,23 @@ def _transform_profile_for_api(profile: dict) -> dict: details.append(f"Drive: {hidden['deep_motive']}") if hidden.get("behavioral_mapping"): details.append(f"Approach: {hidden['behavioral_mapping']}") - experience.append({ - "role": f"{profile.get('title', 'Executive Advisor')}", - "details": details - }) + experience.append( + {"role": f"{profile.get('title', 'Executive Advisor')}", "details": details} + ) if experience: result["experience"] = experience - + # Skills from skills field if profile.get("skills"): result["skills"] = profile["skills"] - + # Signature achievement from world_lore world_lore = profile.get("world_lore", {}) if world_lore.get("habitat"): - result["signature_achievement"] = f"{world_lore['habitat']}. {world_lore.get('lexicon', '')}".strip() - + result["signature_achievement"] = ( + f"{world_lore['habitat']}. {world_lore.get('lexicon', '')}".strip() + ) + return result @@ -175,7 +176,7 @@ async def handle_get_profile(request: web.Request) -> web.Response: profile = load_queen_profile(queen_id) except FileNotFoundError: return web.json_response({"error": f"Queen '{queen_id}' not found"}, status=404) - + api_profile = _transform_profile_for_api(profile) return web.json_response({"id": queen_id, **api_profile}) @@ -224,11 +225,13 @@ async def handle_queen_session(request: web.Request) -> web.Response: # 1. Check for an existing live session bound to this queen. for session in manager.list_sessions(): if session.queen_name == queen_id: - return web.json_response({ - "session_id": session.id, - "queen_id": queen_id, - "status": "live", - }) + return web.json_response( + { + "session_id": session.id, + "queen_id": queen_id, + "status": "live", + } + ) # Stop any live sessions bound to a different queen so only one queen # is active at a time. @@ -267,11 +270,13 @@ async def handle_queen_session(request: web.Request) -> web.Response: ) status = "created" - return web.json_response({ - "session_id": session.id, - "queen_id": queen_id, - "status": status, - }) + return web.json_response( + { + "session_id": session.id, + "queen_id": queen_id, + "status": status, + } + ) async def handle_select_queen_session(request: web.Request) -> web.Response: diff --git a/core/framework/server/routes_sessions.py b/core/framework/server/routes_sessions.py index bb13f952..2e7f34d8 100644 --- a/core/framework/server/routes_sessions.py +++ b/core/framework/server/routes_sessions.py @@ -722,9 +722,7 @@ async def handle_delete_agent(request: web.Request) -> web.Response: # Reject deletion of framework agents (~/.hive/agents/) — those are internal hive_agents_dir = Path.home() / ".hive" / "agents" if resolved.is_relative_to(hive_agents_dir): - return web.json_response( - {"error": "Cannot delete framework agents"}, status=403 - ) + return web.json_response({"error": "Cannot delete framework agents"}, status=403) # Stop any live sessions that use this agent for session in list(manager.list_sessions()): @@ -755,9 +753,11 @@ async def handle_reveal_session_folder(request: web.Request) -> web.Response: storage_session_id = (session.queen_resume_from or session.id) if session else session_id if session: from framework.server.session_manager import _queen_session_dir + folder = _queen_session_dir(storage_session_id, session.queen_name) else: from framework.server.session_manager import _find_queen_session_dir + folder = _find_queen_session_dir(storage_session_id) folder.mkdir(parents=True, exist_ok=True) diff --git a/core/framework/server/session_manager.py b/core/framework/server/session_manager.py index 9a777710..07d08f92 100644 --- a/core/framework/server/session_manager.py +++ b/core/framework/server/session_manager.py @@ -98,7 +98,9 @@ class SessionManager: (blocking I/O) then started on the event loop. """ - def __init__(self, model: str | None = None, credential_store=None, queen_tool_registry=None) -> None: + def __init__( + self, model: str | None = None, credential_store=None, queen_tool_registry=None + ) -> None: self._sessions: dict[str, Session] = {} self._loading: set[str] = set() self._model = model @@ -266,7 +268,12 @@ class SessionManager: session.queen_name = queen_name # Start queen immediately (queen-only, no worker tools yet) - await self._start_queen(session, worker_identity=None, initial_prompt=initial_prompt, initial_phase=initial_phase) + await self._start_queen( + session, + worker_identity=None, + initial_prompt=initial_prompt, + initial_phase=initial_phase, + ) logger.info( "Session '%s' created (queen-only, resume_from=%s)", @@ -352,7 +359,10 @@ class SessionManager: else None ) await self._start_queen( - session, worker_identity=worker_identity, initial_prompt=initial_prompt, initial_phase=initial_phase + session, + worker_identity=worker_identity, + initial_prompt=initial_prompt, + initial_phase=initial_phase, ) except Exception: @@ -752,11 +762,11 @@ class SessionManager: # are persisted before the session is destroyed (fire-and-forget). if session.queen_dir is not None: try: - from framework.agents.queen.reflection_agent import run_shutdown_reflection from framework.agents.queen.queen_memory_v2 import ( global_memory_dir, queen_memory_dir, ) + from framework.agents.queen.reflection_agent import run_shutdown_reflection global_mem_dir = global_memory_dir() queen_mem_dir = queen_memory_dir(session.queen_name) diff --git a/core/framework/server/tests/test_api.py b/core/framework/server/tests/test_api.py index 3252374c..39c6d896 100644 --- a/core/framework/server/tests/test_api.py +++ b/core/framework/server/tests/test_api.py @@ -16,9 +16,12 @@ from aiohttp.test_utils import TestClient, TestServer from framework.host.triggers import TriggerDefinition from framework.llm.model_catalog import get_models_catalogue +from framework.server import ( + routes_messages, + routes_queens, + session_manager as session_manager_module, +) from framework.server.app import create_app -from framework.server import routes_messages, routes_queens -from framework.server import session_manager as session_manager_module from framework.server.session_manager import Session REPO_ROOT = Path(__file__).resolve().parents[4] @@ -299,7 +302,9 @@ def _write_sample_session(base: Path, session_id: str): return session_id, session_dir, state -def _write_queen_session(tmp_path: Path, queen_id: str, session_id: str, meta: dict | None = None) -> Path: +def _write_queen_session( + tmp_path: Path, queen_id: str, session_id: str, meta: dict | None = None +) -> Path: """Create a persisted queen session directory for restore tests.""" session_dir = tmp_path / ".hive" / "agents" / "queens" / queen_id / "sessions" / session_id session_dir.mkdir(parents=True) @@ -573,6 +578,7 @@ class TestSessionCRUD: ) assert resp.status == 400 + class TestMessageBootstrap: @pytest.mark.asyncio async def test_new_message_requires_non_empty_message(self): @@ -593,7 +599,9 @@ class TestMessageBootstrap: created = _make_session(agent_id="fresh_queen_session", with_queen=False) created.queen_name = "queen_technology" manager.create_session = AsyncMock(return_value=created) - monkeypatch.setattr(routes_messages, "select_queen", AsyncMock(return_value="queen_technology")) + monkeypatch.setattr( + routes_messages, "select_queen", AsyncMock(return_value="queen_technology") + ) async with TestClient(TestServer(app)) as client: resp = await client.post("/api/messages/new", json={"message": "Build me a scraper"}) @@ -623,7 +631,9 @@ class TestQueenSessionSelection: @pytest.mark.asyncio async def test_select_queen_session_rejects_foreign_session(self, monkeypatch, tmp_path): _patch_queen_storage(monkeypatch, tmp_path) - _write_queen_session(tmp_path, "queen_growth", "other_session", {"queen_id": "queen_growth"}) + _write_queen_session( + tmp_path, "queen_growth", "other_session", {"queen_id": "queen_growth"} + ) app = create_app() async with TestClient(TestServer(app)) as client: @@ -658,12 +668,12 @@ class TestQueenSessionSelection: "queen_id": "queen_technology", "status": "live", } - assert any( - call.args == ("other_live",) for call in manager.stop_session.await_args_list - ) + assert any(call.args == ("other_live",) for call in manager.stop_session.await_args_list) @pytest.mark.asyncio - async def test_select_queen_session_restores_specific_history_session(self, monkeypatch, tmp_path): + async def test_select_queen_session_restores_specific_history_session( + self, monkeypatch, tmp_path + ): _patch_queen_storage(monkeypatch, tmp_path) _write_queen_session( tmp_path, diff --git a/core/framework/skills/manager.py b/core/framework/skills/manager.py index aff8b131..4a40ff22 100644 --- a/core/framework/skills/manager.py +++ b/core/framework/skills/manager.py @@ -122,10 +122,12 @@ class SkillsManager: # 1. Skill discovery -- always run to pick up framework skills; # community/project skills only when project_root is available. - discovery = SkillDiscovery(DiscoveryConfig( - project_root=self._config.project_root, - skip_framework_scope=False, - )) + discovery = SkillDiscovery( + DiscoveryConfig( + project_root=self._config.project_root, + skip_framework_scope=False, + ) + ) discovered = discovery.discover() self._watched_dirs = discovery.scanned_directories @@ -254,8 +256,11 @@ class SkillsManager: self._loaded = False self._do_load() self._loaded = True - logger.info("Skills reloaded: protocols=%d chars, catalog=%d chars", - len(self._protocols_prompt), len(self._catalog_prompt)) + logger.info( + "Skills reloaded: protocols=%d chars, catalog=%d chars", + len(self._protocols_prompt), + len(self._catalog_prompt), + ) # ------------------------------------------------------------------ # Prompt accessors (consumed by downstream layers) diff --git a/core/framework/storage/migrate_v2.py b/core/framework/storage/migrate_v2.py index 33273926..c64cee7e 100644 --- a/core/framework/storage/migrate_v2.py +++ b/core/framework/storage/migrate_v2.py @@ -11,7 +11,6 @@ Safe to re-run (skips already-migrated items). from __future__ import annotations -import json import logging import shutil from pathlib import Path @@ -90,9 +89,7 @@ def _migrate_queen_sessions() -> None: session_dir.rename(target) migrated += 1 except OSError: - logger.warning( - "migrate_v2: failed to move session %s", session_dir, exc_info=True - ) + logger.warning("migrate_v2: failed to move session %s", session_dir, exc_info=True) if migrated: logger.info("migrate_v2: moved %d queen session(s) to new path", migrated) diff --git a/core/framework/tools/migrate_agent.py b/core/framework/tools/migrate_agent.py index 52119c60..8945d27d 100644 --- a/core/framework/tools/migrate_agent.py +++ b/core/framework/tools/migrate_agent.py @@ -239,9 +239,12 @@ def write_yaml(config: dict, output_path: Path) -> None: with open(output_path, "w") as f: yaml.dump( - config, f, - default_flow_style=False, sort_keys=False, - allow_unicode=True, width=120, + config, + f, + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + width=120, ) logger.info("Wrote %s", output_path) diff --git a/core/framework/tools/queen_lifecycle_tools.py b/core/framework/tools/queen_lifecycle_tools.py index 228110f4..2f19d9cb 100644 --- a/core/framework/tools/queen_lifecycle_tools.py +++ b/core/framework/tools/queen_lifecycle_tools.py @@ -43,8 +43,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any from framework.credentials.models import CredentialError -from framework.loader.preload_validation import credential_errors_to_json, validate_credentials from framework.host.event_bus import AgentEvent, EventType +from framework.loader.preload_validation import credential_errors_to_json, validate_credentials from framework.server.app import validate_agent_path from framework.tools.flowchart_utils import ( FLOWCHART_TYPES, @@ -55,9 +55,9 @@ from framework.tools.flowchart_utils import ( ) if TYPE_CHECKING: - from framework.loader.tool_registry import ToolRegistry from framework.host.agent_host import AgentHost from framework.host.event_bus import EventBus + from framework.loader.tool_registry import ToolRegistry logger = logging.getLogger(__name__) @@ -90,7 +90,9 @@ class QueenPhaseState: that trigger phase transitions. """ - phase: str = "building" # "independent", "planning", "building", "staging", "running", or "editing" + phase: str = ( + "building" # "independent", "planning", "building", "staging", "running", or "editing" + ) planning_tools: list = field(default_factory=list) # list[Tool] building_tools: list = field(default_factory=list) # list[Tool] staging_tools: list = field(default_factory=list) # list[Tool] diff --git a/core/framework/tools/session_graph_tools.py b/core/framework/tools/session_graph_tools.py index cf5b308c..de8b9574 100644 --- a/core/framework/tools/session_graph_tools.py +++ b/core/framework/tools/session_graph_tools.py @@ -21,8 +21,8 @@ import logging from typing import TYPE_CHECKING if TYPE_CHECKING: - from framework.loader.tool_registry import ToolRegistry from framework.host.agent_host import AgentHost + from framework.loader.tool_registry import ToolRegistry logger = logging.getLogger(__name__) @@ -46,8 +46,8 @@ def register_graph_tools(registry: ToolRegistry, runtime: AgentHost) -> int: are registered as a secondary graph on the runtime. Returns a JSON summary. """ - from framework.loader.agent_loader import AgentLoader from framework.host.execution_manager import EntryPointSpec + from framework.loader.agent_loader import AgentLoader from framework.server.app import validate_agent_path try: diff --git a/core/pyproject.toml b/core/pyproject.toml index d79e1022..ffe33e30 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -39,7 +39,7 @@ packages = ["framework"] [tool.ruff] target-version = "py311" -line-length = 100 +line-length = 120 lint.select = [ "B", # bugbear errors diff --git a/core/tests/test_queen_memory.py b/core/tests/test_queen_memory.py index 3b840ccd..2101017a 100644 --- a/core/tests/test_queen_memory.py +++ b/core/tests/test_queen_memory.py @@ -259,7 +259,9 @@ def test_format_recall_injection(tmp_path: Path): def test_format_recall_injection_custom_label(tmp_path: Path): (tmp_path / "a.md").write_text("---\nname: a\n---\nbody of a") - result = format_recall_injection(["a.md"], memory_dir=tmp_path, label="Queen Memories: queen_technology") + result = format_recall_injection( + ["a.md"], memory_dir=tmp_path, label="Queen Memories: queen_technology" + ) assert "Queen Memories: queen_technology" in result assert "body of a" in result diff --git a/pyproject.toml b/pyproject.toml index ddbd88ab..29a3de8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,4 +2,4 @@ members = ["core", "tools"] [tool.ruff] -line-length = 100 +line-length = 120 diff --git a/scripts/llm_debug_log_visualizer.py b/scripts/llm_debug_log_visualizer.py index 5ae1298d..8b49c086 100644 --- a/scripts/llm_debug_log_visualizer.py +++ b/scripts/llm_debug_log_visualizer.py @@ -1020,9 +1020,7 @@ def _discover_session_summaries( # Filter out test sessions if needed if not include_tests: by_session = { - eid: recs - for eid, recs in by_session.items() - if not _is_test_session(eid, recs) + eid: recs for eid, recs in by_session.items() if not _is_test_session(eid, recs) } summaries: list[SessionSummary] = [] @@ -1068,14 +1066,10 @@ def main() -> int: logs_dir = args.logs_dir.expanduser() # Only discover summaries, not full session data - summaries = _discover_session_summaries( - logs_dir, args.limit_files, args.include_tests - ) + summaries = _discover_session_summaries(logs_dir, args.limit_files, args.include_tests) initial_session_id = args.session or (summaries[0].execution_id if summaries else "") - if initial_session_id and not any( - s.execution_id == initial_session_id for s in summaries - ): + if initial_session_id and not any(s.execution_id == initial_session_id for s in summaries): print(f"session not found: {initial_session_id}") return 1 diff --git a/tools/coder_tools_server.py b/tools/coder_tools_server.py index f5f480fb..6fd82ad2 100644 --- a/tools/coder_tools_server.py +++ b/tools/coder_tools_server.py @@ -854,6 +854,7 @@ def _validate_agent_tools_impl(agent_path: str) -> dict: try: with open(agent_json_file, encoding="utf-8") as f: data = json.load(f) + # Build lightweight node stubs with .tools and .id/.name class _NodeStub: def __init__(self, d): @@ -866,6 +867,7 @@ def _validate_agent_tools_impl(agent_path: str) -> dict: self.tools = t else: self.tools = [] + nodes = [_NodeStub(n) for n in data.get("nodes", [])] except Exception as e: return {"error": f"Failed to parse agent.json: {e}"} @@ -1542,8 +1544,7 @@ def validate_agent_package(agent_name: str) -> str: steps["schema_validation"] = { "passed": result["valid"], "output": ( - f"{result['nodes']} nodes, {result['edges']} edges, " - f"entry={result['entry']}" + f"{result['nodes']} nodes, {result['edges']} edges, entry={result['entry']}" ), } if result.get("errors"): @@ -1569,8 +1570,12 @@ def validate_agent_package(agent_name: str) -> str: """).format(agent_name=agent_name) proc = subprocess.run( ["uv", "run", "python", "-c", _contract_script], - capture_output=True, text=True, timeout=30, - env=env, cwd=PROJECT_ROOT, stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=30, + env=env, + cwd=PROJECT_ROOT, + stdin=subprocess.DEVNULL, ) if proc.returncode == 0: result = json.loads(proc.stdout.strip()) diff --git a/tools/pyproject.toml b/tools/pyproject.toml index 8f2a85ef..4824126b 100644 --- a/tools/pyproject.toml +++ b/tools/pyproject.toml @@ -92,7 +92,7 @@ packages = ["src/aden_tools"] [tool.ruff] target-version = "py311" -line-length = 100 +line-length = 120 lint.select = [ "B", # bugbear errors diff --git a/tools/src/gcu/browser/bridge.py b/tools/src/gcu/browser/bridge.py index 3c9f1334..4dce1ce0 100644 --- a/tools/src/gcu/browser/bridge.py +++ b/tools/src/gcu/browser/bridge.py @@ -513,7 +513,9 @@ class BeelineBridge: # Check if the element might be inside a Shadow DOM container shadow_hint = "" try: - shadow_check = await self.evaluate(tab_id, """ + shadow_check = await self.evaluate( + tab_id, + """ (function() { var hosts = document.querySelectorAll('[id]'); for (var h of hosts) { @@ -521,7 +523,8 @@ class BeelineBridge: } return null; })() - """) + """, + ) shadow_host = (shadow_check or {}).get("result") if shadow_host: shadow_hint = ( @@ -1083,7 +1086,7 @@ class BeelineBridge: var box = document.createElement('div'); box.id = '__hive_hl'; box.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;' - + 'left:{int(x)}px;top:{int(y)}px;width:{max(1,int(w))}px;height:{max(1,int(h))}px;' + + 'left:{int(x)}px;top:{int(y)}px;width:{max(1, int(w))}px;height:{max(1, int(h))}px;' + 'border:2px solid {border_rgb};background:{bg_rgba};' + 'border-radius:3px;transition:opacity 0.4s ease;opacity:1;' + 'box-shadow:0 0 8px {bg_rgba};'; @@ -1111,8 +1114,12 @@ class BeelineBridge: pass # best-effort visual feedback _interaction_highlights[tab_id] = { - "x": x, "y": y, "w": w, "h": h, - "label": label, "kind": "rect", + "x": x, + "y": y, + "w": w, + "h": h, + "label": label, + "kind": "rect", } async def highlight_point(self, tab_id: int, x: float, y: float, label: str = "") -> None: @@ -1128,7 +1135,7 @@ class BeelineBridge: var dot = document.createElement('div'); dot.id = '__hive_hl'; dot.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;' - + 'left:{int(x)-8}px;top:{int(y)-8}px;width:16px;height:16px;' + + 'left:{int(x) - 8}px;top:{int(y) - 8}px;width:16px;height:16px;' + 'border-radius:50%;background:rgba(239,68,68,0.7);' + 'box-shadow:0 0 0 4px rgba(239,68,68,0.25),0 0 12px rgba(239,68,68,0.4);' + 'transition:opacity 0.4s ease;opacity:1;'; @@ -1155,17 +1162,24 @@ class BeelineBridge: pass _interaction_highlights[tab_id] = { - "x": x, "y": y, "w": 0, "h": 0, - "label": label, "kind": "point", + "x": x, + "y": y, + "w": 0, + "h": 0, + "label": label, + "kind": "point", } async def clear_highlight(self, tab_id: int) -> None: """Remove the injected highlight from the page.""" try: - await self.evaluate(tab_id, """ + await self.evaluate( + tab_id, + """ var el = document.getElementById('__hive_hl'); if (el) el.remove(); - """) + """, + ) except Exception: pass _interaction_highlights.pop(tab_id, None)