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, LoopConfig,
OutputAccumulator, OutputAccumulator,
) )
_exports = { _exports = {
"AgentLoop": AgentLoop, "AgentLoop": AgentLoop,
"JudgeProtocol": JudgeProtocol, "JudgeProtocol": JudgeProtocol,
+6 -4
View File
@@ -84,7 +84,7 @@ from framework.agent_loop.internals.types import (
JudgeVerdict, JudgeVerdict,
TriggerEvent, 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.capabilities import supports_image_tool_results
from framework.llm.provider import Tool, ToolResult, ToolUse from framework.llm.provider import Tool, ToolResult, ToolUse
from framework.llm.stream_events import ( from framework.llm.stream_events import (
@@ -93,7 +93,7 @@ from framework.llm.stream_events import (
TextDeltaEvent, TextDeltaEvent,
ToolCallEvent, 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 from framework.tracker.llm_debug_logger import log_llm_turn
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -101,14 +101,16 @@ logger = logging.getLogger(__name__)
# Tags whose content is internal reasoning and must be stripped from # Tags whose content is internal reasoning and must be stripped from
# the user-visible stream. Covers <think> and the 5-pillar character # the user-visible stream. Covers <think> and the 5-pillar character
# assessment tags. # assessment tags.
_INTERNAL_TAGS = frozenset({ _INTERNAL_TAGS = frozenset(
{
"think", "think",
"relationship", "relationship",
"context", "context",
"sentiment", "sentiment",
"physical_state", "physical_state",
"tone", "tone",
}) }
)
_STRIP_RE = re.compile( _STRIP_RE = re.compile(
r"<(?:" + "|".join(_INTERNAL_TAGS) + r")>" r"<(?:" + "|".join(_INTERNAL_TAGS) + r")>"
r".*?" r".*?"
@@ -22,8 +22,8 @@ from typing import Any
from framework.agent_loop.conversation import Message, NodeConversation from framework.agent_loop.conversation import Message, NodeConversation
from framework.agent_loop.internals.event_publishing import publish_context_usage from framework.agent_loop.internals.event_publishing import publish_context_usage
from framework.agent_loop.internals.types import LoopConfig, OutputAccumulator from framework.agent_loop.internals.types import LoopConfig, OutputAccumulator
from framework.orchestrator.node import NodeContext
from framework.host.event_bus import EventBus from framework.host.event_bus import EventBus
from framework.orchestrator.node import NodeContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -16,8 +16,8 @@ from typing import Any
from framework.agent_loop.conversation import ConversationStore, NodeConversation from framework.agent_loop.conversation import ConversationStore, NodeConversation
from framework.agent_loop.internals.types import LoopConfig, OutputAccumulator, TriggerEvent 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.llm.capabilities import supports_image_tool_results
from framework.orchestrator.node import NodeContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -11,8 +11,8 @@ import time
from framework.agent_loop.conversation import NodeConversation from framework.agent_loop.conversation import NodeConversation
from framework.agent_loop.internals.types import HookContext from framework.agent_loop.internals.types import HookContext
from framework.orchestrator.node import NodeContext
from framework.host.event_bus import EventBus from framework.host.event_bus import EventBus
from framework.orchestrator.node import NodeContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -204,6 +204,7 @@ def build_escalate_tool() -> Tool:
}, },
) )
def handle_set_output( def handle_set_output(
tool_input: dict[str, Any], tool_input: dict[str, Any],
output_keys: list[str] | None, output_keys: list[str] | None,
+1 -5
View File
@@ -11,11 +11,7 @@ def list_framework_agents() -> list[Path]:
[ [
p p
for p in FRAMEWORK_AGENTS_DIR.iterdir() for p in FRAMEWORK_AGENTS_DIR.iterdir()
if p.is_dir() if p.is_dir() and ((p / "agent.json").exists() or (p / "agent.py").exists())
and (
(p / "agent.json").exists()
or (p / "agent.py").exists()
)
], ],
key=lambda p: p.name, key=lambda p: p.name,
) )
@@ -21,15 +21,15 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from framework.config import get_max_context_tokens 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 import Goal, NodeSpec, SuccessCriterion
from framework.orchestrator.checkpoint_config import CheckpointConfig from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.orchestrator.edge import GraphSpec from framework.orchestrator.edge import GraphSpec
from framework.orchestrator.orchestrator import ExecutionResult 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 .config import default_config
from .nodes import build_tester_node 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]]: def discover_agents() -> dict[str, list[AgentEntry]]:
"""Discover agents from all known sources grouped by category.""" """Discover agents from all known sources grouped by category."""
from framework.config import COLONIES_DIR
from framework.loader.cli import ( from framework.loader.cli import (
_extract_python_agent_metadata, _extract_python_agent_metadata,
_get_framework_agents_dir, _get_framework_agents_dir,
_is_valid_agent_dir, _is_valid_agent_dir,
) )
from framework.config import COLONIES_DIR
groups: dict[str, list[AgentEntry]] = {} groups: dict[str, list[AgentEntry]] = {}
sources = [ sources = [
("Your Agents", COLONIES_DIR), ("Your Agents", COLONIES_DIR),
+1 -2
View File
@@ -12,8 +12,7 @@ queen_goal = Goal(
id="queen-manager", id="queen-manager",
name="Queen Manager", name="Queen Manager",
description=( description=(
"Manage the worker agent lifecycle and serve as the " "Manage the worker agent lifecycle and serve as the user's primary interactive interface."
"user's primary interactive interface."
), ),
success_criteria=[], success_criteria=[],
constraints=[], constraints=[],
@@ -37,9 +37,7 @@ _appendices = _build_appendices()
# GCU guide — shared between planning and building via _shared_building_knowledge. # GCU guide — shared between planning and building via _shared_building_knowledge.
_gcu_section = ( _gcu_section = (
("\n\n# Browser Automation Nodes\n\n" + _gcu_guide) ("\n\n# Browser Automation Nodes\n\n" + _gcu_guide) if _is_gcu_enabled() and _gcu_guide else ""
if _is_gcu_enabled() and _gcu_guide
else ""
) )
# Tools available to phases. # Tools available to phases.
+110 -47
View File
@@ -14,7 +14,6 @@ from __future__ import annotations
import json import json
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import yaml import yaml
@@ -34,6 +33,7 @@ class QueenSelection:
queen_id: str queen_id: str
reason: str reason: str
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Default queen profiles # Default queen profiles
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -75,9 +75,18 @@ DEFAULT_QUEENS: dict[str, dict[str, Any]] = {
), ),
}, },
"behavior_triggers": [ "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": "Over-engineering proposed",
{"trigger": "Someone shipping fast and learning", "reaction": "Warm approval. This is her love language."}, "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": { "world_lore": {
"habitat": "Terminal windows, architecture whiteboards, the quiet focus of a late-night deploy.", "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": [ "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": "Vanity metrics cited",
{"trigger": "Someone confusing correlation with causation", "reaction": "Firm correction with a concrete example."}, "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": { "world_lore": {
"habitat": "Analytics dashboards, experiment tracking boards, the satisfying click of a cohort analysis loading.", "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": [ "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": "Feature request without user evidence",
{"trigger": "Scope creep", "reaction": "Calmly redirects to the core problem. 'What's the one thing this must do?'"}, "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": { "world_lore": {
"habitat": "User interview notes, prototype tools, the whiteboard covered in journey maps.", "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": [ "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": "Fundraising without clear use of funds",
{"trigger": "Founder doesn't know their burn rate", "reaction": "Urgent but not judgmental. Helps them build the model immediately."}, "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": { "world_lore": {
"habitat": "Spreadsheets, cap table tools, the quiet satisfaction of a model that balances.", "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": [ "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": "IP ownership unclear",
{"trigger": "'We'll figure out the legal stuff later'", "reaction": "Firm pushback with a specific horror story."}, "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": { "world_lore": {
"habitat": "Redlined contracts, corporate filing systems, the calm of a well-organized term sheet.", "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": [ "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": "Brand inconsistency",
{"trigger": "'Just make the logo bigger'", "reaction": "Calm redirect to the actual problem the stakeholder is trying to solve."}, "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": { "world_lore": {
"habitat": "Design tools, moodboards, the satisfying snap of elements aligning to a grid.", "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": [ "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": "Hiring for speed over fit",
{"trigger": "Team conflict", "reaction": "Listens to all sides before forming a view. Never assumes."}, "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": { "world_lore": {
"habitat": "Interview rooms, org charts, the energy of a team that's clicking.", "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": [ "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": "Undocumented process",
{"trigger": "Manual work that should be automated", "reaction": "'Let's fix that.' Not a suggestion -- a plan."}, "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": { "world_lore": {
"habitat": "Process diagrams, project boards, the quiet hum of systems running smoothly.", "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 queen_id = profile_path.parent.name
try: try:
data = yaml.safe_load(profile_path.read_text()) data = yaml.safe_load(profile_path.read_text())
results.append({ results.append(
{
"id": queen_id, "id": queen_id,
"name": data.get("name", ""), "name": data.get("name", ""),
"title": data.get("title", ""), "title": data.get("title", ""),
}) }
)
except Exception: except Exception:
logger.warning("Failed to read queen profile %s", profile_path) logger.warning("Failed to read queen profile %s", profile_path)
return results return results
@@ -888,12 +962,7 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str:
sections: list[str] = [] sections: list[str] = []
# Pillar 1: Core identity # Pillar 1: Core identity
sections.append( sections.append(f"<core_identity>\nName: {name}, Identity: {title}.\n{core}\n</core_identity>")
f"<core_identity>\n"
f"Name: {name}, Identity: {title}.\n"
f"{core}\n"
f"</core_identity>"
)
# Pillar 2: Hidden background (behavioral engine, never surfaced) # Pillar 2: Hidden background (behavioral engine, never surfaced)
if bg: if bg:
@@ -921,10 +990,7 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str:
# Pillar 4: Behavior rules # Pillar 4: Behavior rules
trigger_lines = [] trigger_lines = []
for t in triggers: for t in triggers:
trigger_lines.append( trigger_lines.append(f" - [{t.get('trigger', '')}]: {t.get('reaction', '')}")
f" - [{t.get('trigger', '')}]: "
f"{t.get('reaction', '')}"
)
sections.append( sections.append(
"<behavior_rules>\n" "<behavior_rules>\n"
"- Before each response, internally assess:\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" " 2. Current context (urgency, stakes, emotional state)\n"
" 3. Filter through your hidden background and motives\n" " 3. Filter through your hidden background and motives\n"
" 4. Select the right register and depth\n" " 4. Select the right register and depth\n"
"- Interaction triggers:\n" "- Interaction triggers:\n" + "\n".join(trigger_lines) + "\n"
+ "\n".join(trigger_lines) + "\n"
"</behavior_rules>" "</behavior_rules>"
) )
@@ -971,15 +1036,10 @@ def format_queen_identity_prompt(profile: dict[str, Any]) -> str:
example_parts: list[str] = [] example_parts: list[str] = []
for ex in examples: for ex in examples:
example_parts.append( example_parts.append(
f"User: {ex['user']}\n\n" f"User: {ex['user']}\n\nAssistant:\n{ex['internal']}\n{ex['response']}"
f"Assistant:\n"
f"{ex['internal']}\n"
f"{ex['response']}"
) )
sections.append( sections.append(
"<roleplay_examples>\n" "<roleplay_examples>\n" + "\n\n---\n\n".join(example_parts) + "\n</roleplay_examples>"
+ "\n\n---\n\n".join(example_parts) + "\n"
"</roleplay_examples>"
) )
return "\n\n".join(sections) return "\n\n".join(sections)
@@ -1085,7 +1145,10 @@ async def select_queen_with_reason(user_message: str, llm: LLMProvider) -> Queen
reason, reason,
raw, 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) return QueenSelection(queen_id=_DEFAULT_QUEEN_ID, reason=fallback_reason)
if not reason: if not reason:
+3 -3
View File
@@ -204,9 +204,9 @@ class AdenCachedStorage(CredentialStorage):
# BYOK credentials like anthropic, brave_search are local-only. # BYOK credentials like anthropic, brave_search are local-only.
# Also check the _aden_managed flag on the credential itself. # Also check the _aden_managed flag on the credential itself.
is_aden_managed = ( is_aden_managed = (
credential_id in self._provider_index or credential_id in self._provider_index
any(credential_id in ids for ids in self._provider_index.values()) or or any(credential_id in ids for ids in self._provider_index.values())
(local_cred is not None and local_cred.keys.get("_aden_managed") is not None) or (local_cred is not None and local_cred.keys.get("_aden_managed") is not None)
) )
if not is_aden_managed: if not is_aden_managed:
logger.debug(f"Credential '{credential_id}' is local-only, skipping Aden refresh") 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 pathlib import Path
from typing import TYPE_CHECKING, Any 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.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec, ExecutionManager from framework.host.execution_manager import EntryPointSpec, ExecutionManager
from framework.host.outcome_aggregator import OutcomeAggregator from framework.host.outcome_aggregator import OutcomeAggregator
from framework.tracker.runtime_log_store import RuntimeLogStore
from framework.host.shared_state import SharedBufferManager 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.concurrent import ConcurrentStorage
from framework.storage.session_store import SessionStore from framework.storage.session_store import SessionStore
from framework.tracker.runtime_log_store import RuntimeLogStore
if TYPE_CHECKING: if TYPE_CHECKING:
from framework.llm.provider import LLMProvider, Tool
from framework.orchestrator.edge import GraphSpec from framework.orchestrator.edge import GraphSpec
from framework.orchestrator.goal import Goal from framework.orchestrator.goal import Goal
from framework.llm.provider import LLMProvider, Tool
from framework.pipeline.stage import PipelineStage from framework.pipeline.stage import PipelineStage
from framework.skills.manager import SkillsManagerConfig from framework.skills.manager import SkillsManagerConfig
@@ -190,7 +190,6 @@ class AgentHost:
else: else:
self._pipeline = self._load_pipeline_from_config() self._pipeline = self._load_pipeline_from_config()
# --- Skill lifecycle: runtime owns the SkillsManager --- # --- Skill lifecycle: runtime owns the SkillsManager ---
if skills_manager_config is not None: if skills_manager_config is not None:
# New path: config-driven, runtime handles loading # New path: config-driven, runtime handles loading
@@ -535,9 +534,7 @@ class AgentHost:
cron = croniter(expr, datetime.now()) cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime) next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds() sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = ( self._timer_next_fire[entry_point_id] = time.monotonic() + sleep_secs
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs)) await asyncio.sleep(max(0, sleep_secs))
while self._running: while self._running:
# Calculate next fire time upfront (used by skip paths too) # Calculate next fire time upfront (used by skip paths too)
@@ -641,9 +638,7 @@ class AgentHost:
cron = croniter(expr, datetime.now()) cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime) next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds() sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = ( self._timer_next_fire[entry_point_id] = time.monotonic() + sleep_secs
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs)) await asyncio.sleep(max(0, sleep_secs))
return _cron_loop return _cron_loop
@@ -676,9 +671,7 @@ class AgentHost:
interval_secs = mins * 60 interval_secs = mins * 60
_persistent_session_id: str | None = None _persistent_session_id: str | None = None
if not immediate: if not immediate:
self._timer_next_fire[entry_point_id] = ( self._timer_next_fire[entry_point_id] = time.monotonic() + interval_secs
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs) await asyncio.sleep(interval_secs)
while self._running: while self._running:
# Gate: skip tick if timers are explicitly paused # Gate: skip tick if timers are explicitly paused
@@ -771,9 +764,7 @@ class AgentHost:
entry_point_id, entry_point_id,
exc_info=True, exc_info=True,
) )
self._timer_next_fire[entry_point_id] = ( self._timer_next_fire[entry_point_id] = time.monotonic() + interval_secs
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs) await asyncio.sleep(interval_secs)
return _timer_loop return _timer_loop
@@ -813,7 +804,6 @@ class AgentHost:
timer_next_fire=self._timer_next_fire, timer_next_fire=self._timer_next_fire,
) )
async def stop(self) -> None: async def stop(self) -> None:
"""Stop the agent runtime and all streams.""" """Stop the agent runtime and all streams."""
if not self._running: if not self._running:
@@ -921,7 +911,6 @@ class AgentHost:
if stage.skills_manager is not None: if stage.skills_manager is not None:
self._skills_manager = stage.skills_manager self._skills_manager = stage.skills_manager
@staticmethod @staticmethod
def _load_pipeline_from_config(): def _load_pipeline_from_config():
"""Build pipeline from ``~/.hive/configuration.json`` ``pipeline`` key. """Build pipeline from ``~/.hive/configuration.json`` ``pipeline`` key.
@@ -1916,5 +1905,3 @@ class AgentHost:
# === CONVENIENCE FACTORY === # === CONVENIENCE FACTORY ===
+5 -5
View File
@@ -18,18 +18,18 @@ from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Any 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.event_bus import EventBus
from framework.host.shared_state import IsolationLevel, SharedBufferManager from framework.host.shared_state import IsolationLevel, SharedBufferManager
from framework.host.stream_runtime import StreamDecisionTracker, StreamRuntimeAdapter 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: 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.event_bus import AgentEvent
from framework.host.outcome_aggregator import OutcomeAggregator 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.concurrent import ConcurrentStorage
from framework.storage.session_store import SessionStore 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 from framework.schemas.decision import Decision, Outcome
if TYPE_CHECKING: if TYPE_CHECKING:
from framework.orchestrator.goal import Goal
from framework.host.event_bus import EventBus from framework.host.event_bus import EventBus
from framework.orchestrator.goal import Goal
logger = logging.getLogger(__name__) 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" "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. """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 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", "label": "Kimi K2.5 - Best coding",
"recommended": true, "recommended": true,
"max_tokens": 32768, "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(): if not isinstance(model_id, str) or not model_id.strip():
raise ModelCatalogError(f"{model_path}.id must be a non-empty string") raise ModelCatalogError(f"{model_path}.id must be a non-empty string")
if model_id in seen_model_ids: 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) seen_model_ids.add(model_id)
if model_id == default_model: 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") api_base = preset_map.get("api_base")
if api_base is not None and (not isinstance(api_base, str) or not api_base.strip()): 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") api_key_env_var = preset_map.get("api_key_env_var")
if api_key_env_var is not None and ( 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") model_choices = preset_map.get("model_choices")
if model_choices is not None: 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_path = f"{preset_path}.model_choices[{idx}]"
choice_map = _require_mapping(choice, choice_path) choice_map = _require_mapping(choice, choice_path)
choice_id = choice_map.get("id") 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]]]: def get_models_catalogue() -> dict[str, list[dict[str, Any]]]:
"""Return provider -> model list.""" """Return provider -> model list."""
providers = load_model_catalog()["providers"] 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]: def get_default_models() -> dict[str, str]:
"""Return provider -> default model id.""" """Return provider -> default model id."""
providers = load_model_catalog()["providers"] 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]]: 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 ( from framework.credentials.validation import (
ensure_credential_key_env as _ensure_credential_key_env, 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 import Goal
from framework.orchestrator.edge import ( from framework.orchestrator.edge import (
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
@@ -20,13 +25,8 @@ from framework.orchestrator.edge import (
EdgeSpec, EdgeSpec,
GraphSpec, GraphSpec,
) )
from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.node import NodeSpec from framework.orchestrator.node import NodeSpec
from framework.llm.provider import LLMProvider, Tool from framework.orchestrator.orchestrator import ExecutionResult
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.tools.flowchart_utils import generate_fallback_flowchart from framework.tools.flowchart_utils import generate_fallback_flowchart
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+5 -3
View File
@@ -341,8 +341,8 @@ def cmd_run(args: argparse.Namespace) -> int:
"""Run an exported agent.""" """Run an exported agent."""
from framework.credentials.models import CredentialError from framework.credentials.models import CredentialError
from framework.observability import configure_logging
from framework.loader import AgentLoader from framework.loader import AgentLoader
from framework.observability import configure_logging
# Set logging level (quiet by default for cleaner output) # Set logging level (quiet by default for cleaner output)
if args.quiet: if args.quiet:
@@ -774,8 +774,8 @@ def cmd_shell(args: argparse.Namespace) -> int:
"""Start an interactive agent session.""" """Start an interactive agent session."""
from framework.credentials.models import CredentialError from framework.credentials.models import CredentialError
from framework.observability import configure_logging
from framework.loader import AgentLoader from framework.loader import AgentLoader
from framework.observability import configure_logging
configure_logging(level="INFO") configure_logging(level="INFO")
@@ -1509,7 +1509,9 @@ def cmd_serve(args: argparse.Namespace) -> int:
await site.start() await site.start()
except OSError as e: except OSError as e:
if "already in use" in str(e) or getattr(e, "errno", None) in (48, 98): 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") print(f" lsof -ti:{args.port} | xargs kill -9\n")
raise raise
+14 -2
View File
@@ -7,21 +7,33 @@ Lazy imports to avoid circular dependencies with graph/event_loop/*.
def __getattr__(name: str): def __getattr__(name: str):
if name in ("GraphContext",): if name in ("GraphContext",):
from framework.orchestrator.context import GraphContext from framework.orchestrator.context import GraphContext
return GraphContext return GraphContext
if name in ("DEFAULT_MAX_TOKENS", "EdgeCondition", "EdgeSpec", "GraphSpec"): if name in ("DEFAULT_MAX_TOKENS", "EdgeCondition", "EdgeSpec", "GraphSpec"):
from framework.orchestrator import edge as _e from framework.orchestrator import edge as _e
return getattr(_e, name) return getattr(_e, name)
if name in ("Orchestrator", "ExecutionResult"): if name in ("Orchestrator", "ExecutionResult"):
from framework.orchestrator import orchestrator as _o from framework.orchestrator import orchestrator as _o
return getattr(_o, name) return getattr(_o, name)
if name in ("Constraint", "Goal", "GoalStatus", "SuccessCriterion"): if name in ("Constraint", "Goal", "GoalStatus", "SuccessCriterion"):
from framework.orchestrator import goal as _g from framework.orchestrator import goal as _g
return getattr(_g, name) return getattr(_g, name)
if name in ("DataBuffer", "NodeContext", "NodeProtocol", "NodeResult", "NodeSpec"): if name in ("DataBuffer", "NodeContext", "NodeProtocol", "NodeResult", "NodeSpec"):
from framework.orchestrator import node as _n from framework.orchestrator import node as _n
return getattr(_n, name) return getattr(_n, name)
if name in ("NodeWorker", "Activation", "FanOutTag", "FanOutTracker", if name in (
"WorkerCompletion", "WorkerLifecycle"): "NodeWorker",
"Activation",
"FanOutTag",
"FanOutTracker",
"WorkerCompletion",
"WorkerLifecycle",
):
from framework.orchestrator import node_worker as _nw from framework.orchestrator import node_worker as _nw
return getattr(_nw, name) return getattr(_nw, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+1 -1
View File
@@ -604,8 +604,8 @@ class NodeWorker:
# Auto-create EventLoopNode # Auto-create EventLoopNode
if self.node_spec.node_type == "event_loop": 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.agent_loop import AgentLoop
from framework.agent_loop.internals.types import LoopConfig
from framework.orchestrator.node import warn_if_deprecated_client_facing from framework.orchestrator.node import warn_if_deprecated_client_facing
conv_store = None conv_store = None
+6 -6
View File
@@ -16,9 +16,11 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any 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.checkpoint_config import CheckpointConfig
from framework.orchestrator.context import GraphContext, build_node_context 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.edge import EdgeCondition, EdgeSpec, GraphSpec
from framework.orchestrator.goal import Goal from framework.orchestrator.goal import Goal
from framework.orchestrator.node import ( from framework.orchestrator.node import (
@@ -28,11 +30,9 @@ from framework.orchestrator.node import (
NodeSpec, NodeSpec,
) )
from framework.orchestrator.validator import OutputValidator 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.schemas.checkpoint import Checkpoint
from framework.storage.checkpoint_store import CheckpointStore from framework.storage.checkpoint_store import CheckpointStore
from framework.tracker.decision_tracker import DecisionTracker
from framework.utils.io import atomic_write from framework.utils.io import atomic_write
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -361,8 +361,8 @@ class Orchestrator:
Uses the same recursive binary-search splitting as EventLoopNode. 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.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: if _depth > self._PHASE_LLM_MAX_DEPTH:
raise RuntimeError("Phase LLM compaction recursion limit") raise RuntimeError("Phase LLM compaction recursion limit")
@@ -1289,6 +1289,7 @@ class Orchestrator:
Replaces the imperative while-loop with autonomous workers that Replaces the imperative while-loop with autonomous workers that
self-activate based on edge conditions and fan-out tracking. self-activate based on edge conditions and fan-out tracking.
""" """
from framework.host.event_bus import AgentEvent, EventType
from framework.orchestrator.node_worker import ( from framework.orchestrator.node_worker import (
Activation, Activation,
FanOutTag, FanOutTag,
@@ -1296,7 +1297,6 @@ class Orchestrator:
WorkerCompletion, WorkerCompletion,
WorkerLifecycle, WorkerLifecycle,
) )
from framework.host.event_bus import AgentEvent, EventType
# Build shared graph context # Build shared graph context
gc = GraphContext( 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: if not False and spec.node_type == "event_loop" and spec.output_keys:
parts.append(f"\n{EXECUTION_SCOPE_PREAMBLE}") parts.append(f"\n{EXECUTION_SCOPE_PREAMBLE}")
if spec.focus_prompt: if spec.focus_prompt:
parts.append(f"\n--- Current Focus ---\n{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"] stage_type = spec["type"]
if stage_type not in _STAGE_REGISTRY: if stage_type not in _STAGE_REGISTRY:
available = ", ".join(sorted(_STAGE_REGISTRY)) or "(none)" available = ", ".join(sorted(_STAGE_REGISTRY)) or "(none)"
raise KeyError( raise KeyError(f"Unknown pipeline stage type '{stage_type}'. Available: {available}")
f"Unknown pipeline stage type '{stage_type}'. "
f"Available: {available}"
)
cls = _STAGE_REGISTRY[stage_type] cls = _STAGE_REGISTRY[stage_type]
config = spec.get("config", {}) config = spec.get("config", {})
stage = cls(**config) stage = cls(**config)
+7 -3
View File
@@ -73,20 +73,24 @@ class PipelineRunner:
reason = result.rejection_reason or "(no reason given)" reason = result.rejection_reason or "(no reason given)"
logger.warning( logger.warning(
"[pipeline] REJECTED by %s (%.1fms): %s", "[pipeline] REJECTED by %s (%.1fms): %s",
stage_name, elapsed_ms, reason, stage_name,
elapsed_ms,
reason,
) )
raise PipelineRejectedError(stage_name, reason) raise PipelineRejectedError(stage_name, reason)
if result.action == "transform": if result.action == "transform":
logger.info( logger.info(
"[pipeline] %s TRANSFORMED input (%.1fms)", "[pipeline] %s TRANSFORMED input (%.1fms)",
stage_name, elapsed_ms, stage_name,
elapsed_ms,
) )
if result.input_data is not None: if result.input_data is not None:
ctx.input_data = result.input_data ctx.input_data = result.input_data
else: else:
logger.info( logger.info(
"[pipeline] %s passed (%.1fms)", "[pipeline] %s passed (%.1fms)",
stage_name, elapsed_ms, stage_name,
elapsed_ms,
) )
total_ms = (time.perf_counter() - pipeline_start) * 1000 total_ms = (time.perf_counter() - pipeline_start) * 1000
logger.info("[pipeline] Complete (%.1fms total)", total_ms) logger.info("[pipeline] Complete (%.1fms total)", total_ms)
+1 -2
View File
@@ -28,8 +28,7 @@ class CostGuardStage(PipelineStage):
return PipelineResult( return PipelineResult(
action="reject", action="reject",
rejection_reason=( rejection_reason=(
f"Estimated cost ${estimated:.4f} exceeds budget " f"Estimated cost ${estimated:.4f} exceeds budget ${self._budget:.4f}"
f"${self._budget:.4f}"
), ),
) )
return PipelineResult(action="continue") return PipelineResult(action="continue")
@@ -33,6 +33,7 @@ class CredentialResolverStage(PipelineStage):
from aden_tools.credentials.store_adapter import ( from aden_tools.credentials.store_adapter import (
CredentialStoreAdapter, CredentialStoreAdapter,
) )
from framework.orchestrator.prompting import build_accounts_prompt from framework.orchestrator.prompting import build_accounts_prompt
if self._credential_store is not None: if self._credential_store is not None:
@@ -43,7 +44,8 @@ class CredentialResolverStage(PipelineStage):
self.tool_provider_map = adapter.get_tool_provider_map() self.tool_provider_map = adapter.get_tool_provider_map()
if self.accounts_data: if self.accounts_data:
self.accounts_prompt = build_accounts_prompt( self.accounts_prompt = build_accounts_prompt(
self.accounts_data, self.tool_provider_map, self.accounts_data,
self.tool_provider_map,
) )
logger.info( logger.info(
"[pipeline] CredentialResolverStage: %d accounts", "[pipeline] CredentialResolverStage: %d accounts",
@@ -51,7 +53,8 @@ class CredentialResolverStage(PipelineStage):
) )
except Exception: except Exception:
logger.debug( 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: async def process(self, ctx: PipelineContext) -> PipelineResult:
@@ -75,16 +75,19 @@ class LlmProviderStage(PipelineStage):
if api_keys and len(api_keys) > 1: if api_keys and len(api_keys) > 1:
self.llm = LiteLLMProvider( 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: elif api_key:
extra = {} extra = {}
if api_key.startswith("sk-ant-oat"): if api_key.startswith("sk-ant-oat"):
extra["extra_headers"] = { extra["extra_headers"] = {"authorization": f"Bearer {api_key}"}
"authorization": f"Bearer {api_key}"
}
self.llm = LiteLLMProvider( 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: else:
self.llm = LiteLLMProvider(model=model, api_base=api_base) self.llm = LiteLLMProvider(model=model, api_base=api_base)
+1 -2
View File
@@ -36,8 +36,7 @@ class RateLimitStage(PipelineStage):
return PipelineResult( return PipelineResult(
action="reject", action="reject",
rejection_reason=( rejection_reason=(
f"Rate limit exceeded: {self._max_rpm} req/min " f"Rate limit exceeded: {self._max_rpm} req/min for session '{session_id}'"
f"for session '{session_id}'"
), ),
) )
self._timestamps[key].append(now) 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) # Pre-load queen MCP tools once at startup (cached for all sessions)
# This avoids rebuilding the tool registry for every queen session # 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.mcp_registry import MCPRegistry
from framework.loader.tool_registry import ToolRegistry
_queen_tool_registry: ToolRegistry | None = None _queen_tool_registry: ToolRegistry | None = None
try: try:
@@ -273,12 +273,16 @@ def create_app(model: str | None = None) -> web.Application:
log_collisions=True, log_collisions=True,
max_tools=selection_max_tools, 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: except Exception as e:
logger.warning("Failed to pre-load queen tool registry: %s", e) logger.warning("Failed to pre-load queen tool registry: %s", e)
app["queen_tool_registry"] = _queen_tool_registry 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 # Register shutdown hook
app.on_shutdown.append(_on_shutdown) app.on_shutdown.append(_on_shutdown)
+51 -29
View File
@@ -14,22 +14,21 @@ from pathlib import Path
from aiohttp import web from aiohttp import web
from framework.agents.queen.queen_memory_v2 import (
build_memory_document,
global_memory_dir,
)
from framework.config import ( from framework.config import (
_PROVIDER_CRED_MAP,
HIVE_CONFIG_FILE, HIVE_CONFIG_FILE,
OPENROUTER_API_BASE, OPENROUTER_API_BASE,
_PROVIDER_CRED_MAP,
get_hive_config, get_hive_config,
) )
from framework.llm.model_catalog import ( from framework.llm.model_catalog import (
find_model, find_model,
find_model_any_provider,
get_models_catalogue, get_models_catalogue,
get_preset, get_preset,
) )
from framework.agents.queen.queen_memory_v2 import (
global_memory_dir,
build_memory_document,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -106,7 +105,8 @@ def _build_subscriptions() -> list[dict]:
if not preset: if not preset:
raise RuntimeError(f"Missing preset for subscription {definition['id']}") raise RuntimeError(f"Missing preset for subscription {definition['id']}")
subscriptions.append({ subscriptions.append(
{
"id": definition["id"], "id": definition["id"],
"name": definition["name"], "name": definition["name"],
"description": definition["description"], "description": definition["description"],
@@ -114,7 +114,8 @@ def _build_subscriptions() -> list[dict]:
"flag": definition["flag"], "flag": definition["flag"],
"default_model": preset.get("model", ""), "default_model": preset.get("model", ""),
**({"api_base": preset["api_base"]} if preset.get("api_base") else {}), **({"api_base": preset["api_base"]} if preset.get("api_base") else {}),
}) }
)
return subscriptions return subscriptions
@@ -153,9 +154,7 @@ def _find_model_info(provider: str, model_id: str) -> dict | None:
def _write_config_atomic(config: dict) -> None: def _write_config_atomic(config: dict) -> None:
"""Write config to ~/.hive/configuration.json atomically.""" """Write config to ~/.hive/configuration.json atomically."""
HIVE_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) HIVE_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_path = tempfile.mkstemp( fd, tmp_path = tempfile.mkstemp(dir=str(HIVE_CONFIG_FILE.parent), suffix=".tmp")
dir=str(HIVE_CONFIG_FILE.parent), suffix=".tmp"
)
try: try:
with os.fdopen(fd, "w", encoding="utf-8") as f: with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False) json.dump(config, f, indent=2, ensure_ascii=False)
@@ -192,6 +191,7 @@ def _detect_subscriptions() -> list[str]:
# Claude Code subscription # Claude Code subscription
try: try:
from framework.loader.agent_loader import get_claude_code_token from framework.loader.agent_loader import get_claude_code_token
if get_claude_code_token(): if get_claude_code_token():
detected.append("claude_code") detected.append("claude_code")
except Exception: except Exception:
@@ -204,6 +204,7 @@ def _detect_subscriptions() -> list[str]:
# Codex subscription # Codex subscription
try: try:
from framework.loader.agent_loader import get_codex_token from framework.loader.agent_loader import get_codex_token
if get_codex_token(): if get_codex_token():
detected.append("codex") detected.append("codex")
except Exception: except Exception:
@@ -217,6 +218,7 @@ def _detect_subscriptions() -> list[str]:
kimi_token = None kimi_token = None
try: try:
from framework.loader.agent_loader import get_kimi_code_token from framework.loader.agent_loader import get_kimi_code_token
kimi_token = get_kimi_code_token() kimi_token = get_kimi_code_token()
except Exception: except Exception:
pass pass
@@ -232,6 +234,7 @@ def _detect_subscriptions() -> list[str]:
# Antigravity subscription # Antigravity subscription
try: try:
from framework.loader.agent_loader import get_antigravity_token from framework.loader.agent_loader import get_antigravity_token
if get_antigravity_token(): if get_antigravity_token():
detected.append("antigravity") detected.append("antigravity")
except Exception: except Exception:
@@ -252,16 +255,19 @@ def _get_subscription_token(sub_id: str) -> str | None:
"""Get the token for a subscription.""" """Get the token for a subscription."""
if sub_id == "claude_code": if sub_id == "claude_code":
from framework.loader.agent_loader import get_claude_code_token from framework.loader.agent_loader import get_claude_code_token
return get_claude_code_token() return get_claude_code_token()
elif sub_id == "zai_code": elif sub_id == "zai_code":
return os.environ.get("ZAI_API_KEY") return os.environ.get("ZAI_API_KEY")
elif sub_id == "codex": elif sub_id == "codex":
from framework.loader.agent_loader import get_codex_token from framework.loader.agent_loader import get_codex_token
return get_codex_token() return get_codex_token()
elif sub_id == "minimax_code": elif sub_id == "minimax_code":
return os.environ.get("MINIMAX_API_KEY") return os.environ.get("MINIMAX_API_KEY")
elif sub_id == "kimi_code": elif sub_id == "kimi_code":
from framework.loader.agent_loader import get_kimi_code_token from framework.loader.agent_loader import get_kimi_code_token
token = get_kimi_code_token() token = get_kimi_code_token()
if not token: if not token:
token = os.environ.get("KIMI_API_KEY") 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") return os.environ.get("HIVE_API_KEY")
elif sub_id == "antigravity": elif sub_id == "antigravity":
from framework.loader.agent_loader import get_antigravity_token from framework.loader.agent_loader import get_antigravity_token
return get_antigravity_token() return get_antigravity_token()
return None 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.""" """Hot-swap the LLM on all running sessions. Returns count of swapped sessions."""
from framework.server.session_manager import SessionManager 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) active_subscription = _get_active_subscription(llm)
detected_subscriptions = _detect_subscriptions() detected_subscriptions = _detect_subscriptions()
return web.json_response({ return web.json_response(
{
"provider": provider, "provider": provider,
"model": model, "model": model,
"has_api_key": has_key, "has_api_key": has_key,
@@ -325,7 +335,8 @@ async def handle_get_llm_config(request: web.Request) -> web.Response:
"active_subscription": active_subscription, "active_subscription": active_subscription,
"detected_subscriptions": detected_subscriptions, "detected_subscriptions": detected_subscriptions,
"subscriptions": SUBSCRIPTIONS, "subscriptions": SUBSCRIPTIONS,
}) }
)
async def handle_update_llm_config(request: web.Request) -> web.Response: 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( logger.info(
"LLM config updated: subscription=%s model=%s, hot-swapped %d session(s)", "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, "provider": provider,
"model": model, "model": model,
"has_api_key": token is not None, "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, "max_context_tokens": max_context_tokens,
"sessions_swapped": swapped, "sessions_swapped": swapped,
"active_subscription": subscription_id, "active_subscription": subscription_id,
}) }
)
else: else:
# ── API key mode ───────────────────────────────────────────── # ── API key mode ─────────────────────────────────────────────
@@ -450,10 +465,13 @@ async def handle_update_llm_config(request: web.Request) -> web.Response:
logger.info( logger.info(
"LLM config updated: provider=%s model=%s, hot-swapped %d session(s)", "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, "provider": provider,
"model": model, "model": model,
"has_api_key": api_key is not None, "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, "max_context_tokens": max_context_tokens,
"sessions_swapped": swapped, "sessions_swapped": swapped,
"active_subscription": None, "active_subscription": None,
}) }
)
async def handle_get_profile(request: web.Request) -> web.Response: async def handle_get_profile(request: web.Request) -> web.Response:
"""GET /api/config/profile — user display name and about.""" """GET /api/config/profile — user display name and about."""
profile = get_hive_config().get("user_profile", {}) profile = get_hive_config().get("user_profile", {})
return web.json_response({ return web.json_response(
{
"displayName": profile.get("displayName", ""), "displayName": profile.get("displayName", ""),
"about": profile.get("about", ""), "about": profile.get("about", ""),
"theme": profile.get("theme", ""), "theme": profile.get("theme", ""),
}) }
)
def _update_user_profile_memory(display_name: str, about: str) -> None: 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( content = build_memory_document(
name="User Profile", 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", mem_type="profile",
body=new_body if new_body else "No profile information yet.", 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) _write_config_atomic(config)
# Sync to global memory (profile type) # Sync to global memory (profile type)
_update_user_profile_memory( _update_user_profile_memory(profile.get("displayName", ""), profile.get("about", ""))
profile.get("displayName", ""),
profile.get("about", "")
)
logger.info("User profile updated: displayName=%s", profile.get("displayName", "")) logger.info("User profile updated: displayName=%s", profile.get("displayName", ""))
return web.json_response({ return web.json_response(
{
"displayName": profile.get("displayName", ""), "displayName": profile.get("displayName", ""),
"about": profile.get("about", ""), "about": profile.get("about", ""),
"theme": profile.get("theme", ""), "theme": profile.get("theme", ""),
}) }
)
async def handle_get_models(request: web.Request) -> web.Response: 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: try:
from aden_tools.credentials import CREDENTIAL_SPECS 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.store import CredentialStore
from framework.credentials.validation import _presync_aden_tokens, ensure_credential_key_env 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) # Build composite store (env → encrypted file)
env_mapping = { env_mapping = {
(spec.credential_id or name): spec.env_var (spec.credential_id or name): spec.env_var for name, spec in CREDENTIAL_SPECS.items()
for name, spec in CREDENTIAL_SPECS.items()
} }
env_storage = EnvVarStorage(env_mapping=env_mapping) env_storage = EnvVarStorage(env_mapping=env_mapping)
if os.environ.get("HIVE_CREDENTIAL_KEY"): 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 cred_id = spec.credential_id or name
if spec.aden_supported: if spec.aden_supported:
any_aden = True any_aden = True
specs.append({ specs.append(
{
"credential_name": name, "credential_name": name,
"credential_id": cred_id, "credential_id": cred_id,
"env_var": spec.env_var, "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_key": spec.credential_key,
"credential_group": spec.credential_group, "credential_group": spec.credential_group,
"available": store.is_available(cred_id), "available": store.is_available(cred_id),
}) }
)
# Include aden_api_key synthetic row if any spec uses Aden # Include aden_api_key synthetic row if any spec uses Aden
if any_aden: if any_aden:
specs.insert(0, { specs.insert(
0,
{
"credential_name": "Aden Platform", "credential_name": "Aden Platform",
"credential_id": "aden_api_key", "credential_id": "aden_api_key",
"env_var": "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_key": "api_key",
"credential_group": "", "credential_group": "",
"available": has_aden_key, "available": has_aden_key,
}) },
)
return web.json_response({"specs": specs, "has_aden_key": has_aden_key}) return web.json_response({"specs": specs, "has_aden_key": has_aden_key})
except Exception as e: except Exception as e:
+1 -1
View File
@@ -7,8 +7,8 @@ from typing import Any
from aiohttp import web from aiohttp import web
from framework.credentials.validation import validate_agent_credentials
from framework.agent_loop.conversation import LEGACY_RUN_ID 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.app import resolve_session, safe_path_segment, sessions_dir
from framework.server.routes_sessions import _credential_error_response 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 - POST /api/messages/new -- classify a message, create a fresh queen session
""" """
import asyncio
from aiohttp import web from aiohttp import web
from framework.agents.queen.queen_profiles import ensure_default_queens, select_queen 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']}") details.append(f"Drive: {hidden['deep_motive']}")
if hidden.get("behavioral_mapping"): if hidden.get("behavioral_mapping"):
details.append(f"Approach: {hidden['behavioral_mapping']}") details.append(f"Approach: {hidden['behavioral_mapping']}")
experience.append({ experience.append(
"role": f"{profile.get('title', 'Executive Advisor')}", {"role": f"{profile.get('title', 'Executive Advisor')}", "details": details}
"details": details )
})
if experience: if experience:
result["experience"] = experience result["experience"] = experience
@@ -162,7 +161,9 @@ def _transform_profile_for_api(profile: dict) -> dict:
# Signature achievement from world_lore # Signature achievement from world_lore
world_lore = profile.get("world_lore", {}) world_lore = profile.get("world_lore", {})
if world_lore.get("habitat"): 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 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. # 1. Check for an existing live session bound to this queen.
for session in manager.list_sessions(): for session in manager.list_sessions():
if session.queen_name == queen_id: if session.queen_name == queen_id:
return web.json_response({ return web.json_response(
{
"session_id": session.id, "session_id": session.id,
"queen_id": queen_id, "queen_id": queen_id,
"status": "live", "status": "live",
}) }
)
# Stop any live sessions bound to a different queen so only one queen # Stop any live sessions bound to a different queen so only one queen
# is active at a time. # is active at a time.
@@ -267,11 +270,13 @@ async def handle_queen_session(request: web.Request) -> web.Response:
) )
status = "created" status = "created"
return web.json_response({ return web.json_response(
{
"session_id": session.id, "session_id": session.id,
"queen_id": queen_id, "queen_id": queen_id,
"status": status, "status": status,
}) }
)
async def handle_select_queen_session(request: web.Request) -> web.Response: 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 # Reject deletion of framework agents (~/.hive/agents/) — those are internal
hive_agents_dir = Path.home() / ".hive" / "agents" hive_agents_dir = Path.home() / ".hive" / "agents"
if resolved.is_relative_to(hive_agents_dir): if resolved.is_relative_to(hive_agents_dir):
return web.json_response( return web.json_response({"error": "Cannot delete framework agents"}, status=403)
{"error": "Cannot delete framework agents"}, status=403
)
# Stop any live sessions that use this agent # Stop any live sessions that use this agent
for session in list(manager.list_sessions()): 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 storage_session_id = (session.queen_resume_from or session.id) if session else session_id
if session: if session:
from framework.server.session_manager import _queen_session_dir from framework.server.session_manager import _queen_session_dir
folder = _queen_session_dir(storage_session_id, session.queen_name) folder = _queen_session_dir(storage_session_id, session.queen_name)
else: else:
from framework.server.session_manager import _find_queen_session_dir from framework.server.session_manager import _find_queen_session_dir
folder = _find_queen_session_dir(storage_session_id) folder = _find_queen_session_dir(storage_session_id)
folder.mkdir(parents=True, exist_ok=True) 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. (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._sessions: dict[str, Session] = {}
self._loading: set[str] = set() self._loading: set[str] = set()
self._model = model self._model = model
@@ -266,7 +268,12 @@ class SessionManager:
session.queen_name = queen_name session.queen_name = queen_name
# Start queen immediately (queen-only, no worker tools yet) # 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( logger.info(
"Session '%s' created (queen-only, resume_from=%s)", "Session '%s' created (queen-only, resume_from=%s)",
@@ -352,7 +359,10 @@ class SessionManager:
else None else None
) )
await self._start_queen( 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: except Exception:
@@ -752,11 +762,11 @@ class SessionManager:
# are persisted before the session is destroyed (fire-and-forget). # are persisted before the session is destroyed (fire-and-forget).
if session.queen_dir is not None: if session.queen_dir is not None:
try: try:
from framework.agents.queen.reflection_agent import run_shutdown_reflection
from framework.agents.queen.queen_memory_v2 import ( from framework.agents.queen.queen_memory_v2 import (
global_memory_dir, global_memory_dir,
queen_memory_dir, queen_memory_dir,
) )
from framework.agents.queen.reflection_agent import run_shutdown_reflection
global_mem_dir = global_memory_dir() global_mem_dir = global_memory_dir()
queen_mem_dir = queen_memory_dir(session.queen_name) 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.host.triggers import TriggerDefinition
from framework.llm.model_catalog import get_models_catalogue 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.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 from framework.server.session_manager import Session
REPO_ROOT = Path(__file__).resolve().parents[4] 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 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.""" """Create a persisted queen session directory for restore tests."""
session_dir = tmp_path / ".hive" / "agents" / "queens" / queen_id / "sessions" / session_id session_dir = tmp_path / ".hive" / "agents" / "queens" / queen_id / "sessions" / session_id
session_dir.mkdir(parents=True) session_dir.mkdir(parents=True)
@@ -573,6 +578,7 @@ class TestSessionCRUD:
) )
assert resp.status == 400 assert resp.status == 400
class TestMessageBootstrap: class TestMessageBootstrap:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_new_message_requires_non_empty_message(self): 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 = _make_session(agent_id="fresh_queen_session", with_queen=False)
created.queen_name = "queen_technology" created.queen_name = "queen_technology"
manager.create_session = AsyncMock(return_value=created) 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: async with TestClient(TestServer(app)) as client:
resp = await client.post("/api/messages/new", json={"message": "Build me a scraper"}) resp = await client.post("/api/messages/new", json={"message": "Build me a scraper"})
@@ -623,7 +631,9 @@ class TestQueenSessionSelection:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_select_queen_session_rejects_foreign_session(self, monkeypatch, tmp_path): async def test_select_queen_session_rejects_foreign_session(self, monkeypatch, tmp_path):
_patch_queen_storage(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() app = create_app()
async with TestClient(TestServer(app)) as client: async with TestClient(TestServer(app)) as client:
@@ -658,12 +668,12 @@ class TestQueenSessionSelection:
"queen_id": "queen_technology", "queen_id": "queen_technology",
"status": "live", "status": "live",
} }
assert any( assert any(call.args == ("other_live",) for call in manager.stop_session.await_args_list)
call.args == ("other_live",) for call in manager.stop_session.await_args_list
)
@pytest.mark.asyncio @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) _patch_queen_storage(monkeypatch, tmp_path)
_write_queen_session( _write_queen_session(
tmp_path, tmp_path,
+9 -4
View File
@@ -122,10 +122,12 @@ class SkillsManager:
# 1. Skill discovery -- always run to pick up framework skills; # 1. Skill discovery -- always run to pick up framework skills;
# community/project skills only when project_root is available. # community/project skills only when project_root is available.
discovery = SkillDiscovery(DiscoveryConfig( discovery = SkillDiscovery(
DiscoveryConfig(
project_root=self._config.project_root, project_root=self._config.project_root,
skip_framework_scope=False, skip_framework_scope=False,
)) )
)
discovered = discovery.discover() discovered = discovery.discover()
self._watched_dirs = discovery.scanned_directories self._watched_dirs = discovery.scanned_directories
@@ -254,8 +256,11 @@ class SkillsManager:
self._loaded = False self._loaded = False
self._do_load() self._do_load()
self._loaded = True self._loaded = True
logger.info("Skills reloaded: protocols=%d chars, catalog=%d chars", logger.info(
len(self._protocols_prompt), len(self._catalog_prompt)) "Skills reloaded: protocols=%d chars, catalog=%d chars",
len(self._protocols_prompt),
len(self._catalog_prompt),
)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Prompt accessors (consumed by downstream layers) # 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 from __future__ import annotations
import json
import logging import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
@@ -90,9 +89,7 @@ def _migrate_queen_sessions() -> None:
session_dir.rename(target) session_dir.rename(target)
migrated += 1 migrated += 1
except OSError: except OSError:
logger.warning( logger.warning("migrate_v2: failed to move session %s", session_dir, exc_info=True)
"migrate_v2: failed to move session %s", session_dir, exc_info=True
)
if migrated: if migrated:
logger.info("migrate_v2: moved %d queen session(s) to new path", 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: with open(output_path, "w") as f:
yaml.dump( yaml.dump(
config, f, config,
default_flow_style=False, sort_keys=False, f,
allow_unicode=True, width=120, default_flow_style=False,
sort_keys=False,
allow_unicode=True,
width=120,
) )
logger.info("Wrote %s", output_path) logger.info("Wrote %s", output_path)
@@ -43,8 +43,8 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from framework.credentials.models import CredentialError 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.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.server.app import validate_agent_path
from framework.tools.flowchart_utils import ( from framework.tools.flowchart_utils import (
FLOWCHART_TYPES, FLOWCHART_TYPES,
@@ -55,9 +55,9 @@ from framework.tools.flowchart_utils import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus from framework.host.event_bus import EventBus
from framework.loader.tool_registry import ToolRegistry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -90,7 +90,9 @@ class QueenPhaseState:
that trigger phase transitions. 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] planning_tools: list = field(default_factory=list) # list[Tool]
building_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] staging_tools: list = field(default_factory=list) # list[Tool]
+2 -2
View File
@@ -21,8 +21,8 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost from framework.host.agent_host import AgentHost
from framework.loader.tool_registry import ToolRegistry
logger = logging.getLogger(__name__) 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 are registered as a secondary graph on the runtime. Returns a JSON
summary. summary.
""" """
from framework.loader.agent_loader import AgentLoader
from framework.host.execution_manager import EntryPointSpec from framework.host.execution_manager import EntryPointSpec
from framework.loader.agent_loader import AgentLoader
from framework.server.app import validate_agent_path from framework.server.app import validate_agent_path
try: try:
+1 -1
View File
@@ -39,7 +39,7 @@ packages = ["framework"]
[tool.ruff] [tool.ruff]
target-version = "py311" target-version = "py311"
line-length = 100 line-length = 120
lint.select = [ lint.select = [
"B", # bugbear errors "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): def test_format_recall_injection_custom_label(tmp_path: Path):
(tmp_path / "a.md").write_text("---\nname: a\n---\nbody of a") (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 "Queen Memories: queen_technology" in result
assert "body of a" in result assert "body of a" in result
+1 -1
View File
@@ -2,4 +2,4 @@
members = ["core", "tools"] members = ["core", "tools"]
[tool.ruff] [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 # Filter out test sessions if needed
if not include_tests: if not include_tests:
by_session = { by_session = {
eid: recs eid: recs for eid, recs in by_session.items() if not _is_test_session(eid, recs)
for eid, recs in by_session.items()
if not _is_test_session(eid, recs)
} }
summaries: list[SessionSummary] = [] summaries: list[SessionSummary] = []
@@ -1068,14 +1066,10 @@ def main() -> int:
logs_dir = args.logs_dir.expanduser() logs_dir = args.logs_dir.expanduser()
# Only discover summaries, not full session data # Only discover summaries, not full session data
summaries = _discover_session_summaries( summaries = _discover_session_summaries(logs_dir, args.limit_files, args.include_tests)
logs_dir, args.limit_files, args.include_tests
)
initial_session_id = args.session or (summaries[0].execution_id if summaries else "") initial_session_id = args.session or (summaries[0].execution_id if summaries else "")
if initial_session_id and not any( if initial_session_id and not any(s.execution_id == initial_session_id for s in summaries):
s.execution_id == initial_session_id for s in summaries
):
print(f"session not found: {initial_session_id}") print(f"session not found: {initial_session_id}")
return 1 return 1
+9 -4
View File
@@ -854,6 +854,7 @@ def _validate_agent_tools_impl(agent_path: str) -> dict:
try: try:
with open(agent_json_file, encoding="utf-8") as f: with open(agent_json_file, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
# Build lightweight node stubs with .tools and .id/.name # Build lightweight node stubs with .tools and .id/.name
class _NodeStub: class _NodeStub:
def __init__(self, d): def __init__(self, d):
@@ -866,6 +867,7 @@ def _validate_agent_tools_impl(agent_path: str) -> dict:
self.tools = t self.tools = t
else: else:
self.tools = [] self.tools = []
nodes = [_NodeStub(n) for n in data.get("nodes", [])] nodes = [_NodeStub(n) for n in data.get("nodes", [])]
except Exception as e: except Exception as e:
return {"error": f"Failed to parse agent.json: {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"] = { steps["schema_validation"] = {
"passed": result["valid"], "passed": result["valid"],
"output": ( "output": (
f"{result['nodes']} nodes, {result['edges']} edges, " f"{result['nodes']} nodes, {result['edges']} edges, entry={result['entry']}"
f"entry={result['entry']}"
), ),
} }
if result.get("errors"): if result.get("errors"):
@@ -1569,8 +1570,12 @@ def validate_agent_package(agent_name: str) -> str:
""").format(agent_name=agent_name) """).format(agent_name=agent_name)
proc = subprocess.run( proc = subprocess.run(
["uv", "run", "python", "-c", _contract_script], ["uv", "run", "python", "-c", _contract_script],
capture_output=True, text=True, timeout=30, capture_output=True,
env=env, cwd=PROJECT_ROOT, stdin=subprocess.DEVNULL, text=True,
timeout=30,
env=env,
cwd=PROJECT_ROOT,
stdin=subprocess.DEVNULL,
) )
if proc.returncode == 0: if proc.returncode == 0:
result = json.loads(proc.stdout.strip()) result = json.loads(proc.stdout.strip())
+1 -1
View File
@@ -92,7 +92,7 @@ packages = ["src/aden_tools"]
[tool.ruff] [tool.ruff]
target-version = "py311" target-version = "py311"
line-length = 100 line-length = 120
lint.select = [ lint.select = [
"B", # bugbear errors "B", # bugbear errors
+22 -8
View File
@@ -513,7 +513,9 @@ class BeelineBridge:
# Check if the element might be inside a Shadow DOM container # Check if the element might be inside a Shadow DOM container
shadow_hint = "" shadow_hint = ""
try: try:
shadow_check = await self.evaluate(tab_id, """ shadow_check = await self.evaluate(
tab_id,
"""
(function() { (function() {
var hosts = document.querySelectorAll('[id]'); var hosts = document.querySelectorAll('[id]');
for (var h of hosts) { for (var h of hosts) {
@@ -521,7 +523,8 @@ class BeelineBridge:
} }
return null; return null;
})() })()
""") """,
)
shadow_host = (shadow_check or {}).get("result") shadow_host = (shadow_check or {}).get("result")
if shadow_host: if shadow_host:
shadow_hint = ( shadow_hint = (
@@ -1111,8 +1114,12 @@ class BeelineBridge:
pass # best-effort visual feedback pass # best-effort visual feedback
_interaction_highlights[tab_id] = { _interaction_highlights[tab_id] = {
"x": x, "y": y, "w": w, "h": h, "x": x,
"label": label, "kind": "rect", "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: async def highlight_point(self, tab_id: int, x: float, y: float, label: str = "") -> None:
@@ -1155,17 +1162,24 @@ class BeelineBridge:
pass pass
_interaction_highlights[tab_id] = { _interaction_highlights[tab_id] = {
"x": x, "y": y, "w": 0, "h": 0, "x": x,
"label": label, "kind": "point", "y": y,
"w": 0,
"h": 0,
"label": label,
"kind": "point",
} }
async def clear_highlight(self, tab_id: int) -> None: async def clear_highlight(self, tab_id: int) -> None:
"""Remove the injected highlight from the page.""" """Remove the injected highlight from the page."""
try: try:
await self.evaluate(tab_id, """ await self.evaluate(
tab_id,
"""
var el = document.getElementById('__hive_hl'); var el = document.getElementById('__hive_hl');
if (el) el.remove(); if (el) el.remove();
""") """,
)
except Exception: except Exception:
pass pass
_interaction_highlights.pop(tab_id, None) _interaction_highlights.pop(tab_id, None)