chore: ruff lint

This commit is contained in:
Richard Tang
2026-04-09 18:22:16 -07:00
parent b812f6a03a
commit 9ad95fde59
52 changed files with 527 additions and 370 deletions
+1
View File
@@ -21,6 +21,7 @@ def __getattr__(name: str):
LoopConfig,
OutputAccumulator,
)
_exports = {
"AgentLoop": AgentLoop,
"JudgeProtocol": JudgeProtocol,
+6 -4
View File
@@ -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 <think> and the 5-pillar character
# assessment tags.
_INTERNAL_TAGS = frozenset({
_INTERNAL_TAGS = frozenset(
{
"think",
"relationship",
"context",
"sentiment",
"physical_state",
"tone",
})
}
)
_STRIP_RE = re.compile(
r"<(?:" + "|".join(_INTERNAL_TAGS) + r")>"
r".*?"
@@ -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__)
@@ -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__)
@@ -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__)
@@ -204,6 +204,7 @@ def build_escalate_tool() -> Tool:
},
)
def handle_set_output(
tool_input: dict[str, Any],
output_keys: list[str] | None,
+1 -5
View File
@@ -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,
)
@@ -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
+1 -2
View File
@@ -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),
+1 -2
View File
@@ -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=[],
@@ -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.
+110 -47
View File
@@ -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({
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"<core_identity>\n"
f"Name: {name}, Identity: {title}.\n"
f"{core}\n"
f"</core_identity>"
)
sections.append(f"<core_identity>\nName: {name}, Identity: {title}.\n{core}\n</core_identity>")
# 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(
"<behavior_rules>\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"
"</behavior_rules>"
)
@@ -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(
"<roleplay_examples>\n"
+ "\n\n---\n\n".join(example_parts) + "\n"
"</roleplay_examples>"
"<roleplay_examples>\n" + "\n\n---\n\n".join(example_parts) + "\n</roleplay_examples>"
)
return "\n\n".join(sections)
@@ -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:
+3 -3
View File
@@ -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")
+8 -21
View File
@@ -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
@@ -813,7 +804,6 @@ class AgentHost:
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 ===
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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__)
+3 -1
View File
@@ -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
+1 -1
View File
@@ -250,7 +250,7 @@
"label": "Kimi K2.5 - Best coding",
"recommended": true,
"max_tokens": 32768,
"max_context_tokens": 240000
"max_context_tokens": 200000
}
]
},
+17 -5
View File
@@ -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]]:
+6 -6
View File
@@ -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__)
+5 -3
View File
@@ -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
+14 -2
View File
@@ -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}")
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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(
-2
View File
@@ -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}")
+1 -4
View File
@@ -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)
+7 -3
View File
@@ -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)
+1 -2
View File
@@ -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")
@@ -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:
@@ -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)
+1 -2
View File
@@ -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)
+7 -3
View File
@@ -247,8 +247,8 @@ def create_app(model: str | None = None) -> web.Application:
# 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:
@@ -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)
+51 -29
View File
@@ -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,7 +105,8 @@ def _build_subscriptions() -> list[dict]:
if not preset:
raise RuntimeError(f"Missing preset for subscription {definition['id']}")
subscriptions.append({
subscriptions.append(
{
"id": definition["id"],
"name": definition["name"],
"description": definition["description"],
@@ -114,7 +114,8 @@ def _build_subscriptions() -> list[dict]:
"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,7 +324,8 @@ 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({
return web.json_response(
{
"provider": provider,
"model": model,
"has_api_key": has_key,
@@ -325,7 +335,8 @@ async def handle_get_llm_config(request: web.Request) -> web.Response:
"active_subscription": active_subscription,
"detected_subscriptions": detected_subscriptions,
"subscriptions": SUBSCRIPTIONS,
})
}
)
async def handle_update_llm_config(request: web.Request) -> web.Response:
@@ -393,10 +404,13 @@ 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({
return web.json_response(
{
"provider": provider,
"model": model,
"has_api_key": token is not None,
@@ -404,7 +418,8 @@ async def handle_update_llm_config(request: web.Request) -> web.Response:
"max_context_tokens": max_context_tokens,
"sessions_swapped": swapped,
"active_subscription": subscription_id,
})
}
)
else:
# ── API key mode ─────────────────────────────────────────────
@@ -450,10 +465,13 @@ 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({
return web.json_response(
{
"provider": provider,
"model": model,
"has_api_key": api_key is not None,
@@ -461,17 +479,20 @@ async def handle_update_llm_config(request: web.Request) -> web.Response:
"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({
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:
@@ -525,7 +546,9 @@ def _update_user_profile_memory(display_name: str, about: str) -> None:
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.",
)
@@ -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({
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:
+15 -7
View File
@@ -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,7 +243,8 @@ 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({
specs.append(
{
"credential_name": name,
"credential_id": cred_id,
"env_var": spec.env_var,
@@ -253,11 +257,14 @@ async def handle_list_specs(request: web.Request) -> web.Response:
"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, {
specs.insert(
0,
{
"credential_name": "Aden Platform",
"credential_id": "aden_api_key",
"env_var": "ADEN_API_KEY",
@@ -270,7 +277,8 @@ async def handle_list_specs(request: web.Request) -> web.Response:
"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:
+1 -1
View File
@@ -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
-1
View File
@@ -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
+14 -9
View File
@@ -148,10 +148,9 @@ 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
@@ -162,7 +161,9 @@ def _transform_profile_for_api(profile: dict) -> dict:
# 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
@@ -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({
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({
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:
+3 -3
View File
@@ -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)
+14 -4
View File
@@ -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)
+19 -9
View File
@@ -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,
+9 -4
View File
@@ -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(
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)
+1 -4
View File
@@ -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)
+6 -3
View File
@@ -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)
@@ -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]
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -39,7 +39,7 @@ packages = ["framework"]
[tool.ruff]
target-version = "py311"
line-length = 100
line-length = 120
lint.select = [
"B", # bugbear errors
+3 -1
View File
@@ -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
+1 -1
View File
@@ -2,4 +2,4 @@
members = ["core", "tools"]
[tool.ruff]
line-length = 100
line-length = 120
+3 -9
View File
@@ -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
+9 -4
View File
@@ -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())
+1 -1
View File
@@ -92,7 +92,7 @@ packages = ["src/aden_tools"]
[tool.ruff]
target-version = "py311"
line-length = 100
line-length = 120
lint.select = [
"B", # bugbear errors
+22 -8
View File
@@ -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 = (
@@ -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:
@@ -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)