chore: ruff lint
This commit is contained in:
@@ -21,6 +21,7 @@ def __getattr__(name: str):
|
||||
LoopConfig,
|
||||
OutputAccumulator,
|
||||
)
|
||||
|
||||
_exports = {
|
||||
"AgentLoop": AgentLoop,
|
||||
"JudgeProtocol": JudgeProtocol,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -1060,7 +1120,7 @@ async def select_queen_with_reason(user_message: str, llm: LLMProvider) -> Queen
|
||||
# Find the first '{' and last '}' to extract the JSON object
|
||||
start = raw.find("{")
|
||||
end = raw.rfind("}")
|
||||
json_str = raw[start:end+1] if start != -1 and end != -1 and end > start else raw
|
||||
json_str = raw[start : end + 1] if start != -1 and end != -1 and end > start else raw
|
||||
try:
|
||||
parsed = json.loads(json_str)
|
||||
except json.JSONDecodeError as exc:
|
||||
@@ -1085,7 +1145,10 @@ async def select_queen_with_reason(user_message: str, llm: LLMProvider) -> Queen
|
||||
reason,
|
||||
raw,
|
||||
)
|
||||
fallback_reason = reason or f"Selection failed because the classifier returned unknown queen_id {queen_id!r}."
|
||||
fallback_reason = (
|
||||
reason
|
||||
or f"Selection failed because the classifier returned unknown queen_id {queen_id!r}."
|
||||
)
|
||||
return QueenSelection(queen_id=_DEFAULT_QUEEN_ID, reason=fallback_reason)
|
||||
|
||||
if not reason:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -745,7 +745,9 @@ class LiteLLMProvider(LLMProvider):
|
||||
"LiteLLM is not installed. Please install it with: uv pip install litellm"
|
||||
)
|
||||
|
||||
def reconfigure(self, model: str, api_key: str | None = None, api_base: str | None = None) -> None:
|
||||
def reconfigure(
|
||||
self, model: str, api_key: str | None = None, api_base: str | None = None
|
||||
) -> None:
|
||||
"""Hot-swap the model, API key, and/or base URL on this provider instance.
|
||||
|
||||
Since the same LiteLLMProvider object is shared by reference across the
|
||||
@@ -756,11 +758,11 @@ class LiteLLMProvider(LLMProvider):
|
||||
if _is_ollama_model(model):
|
||||
model = _ensure_ollama_chat_prefix(model)
|
||||
elif model.lower().startswith("kimi/"):
|
||||
model = "anthropic/" + model[len("kimi/"):]
|
||||
model = "anthropic/" + model[len("kimi/") :]
|
||||
if api_base and api_base.rstrip("/").endswith("/v1"):
|
||||
api_base = api_base.rstrip("/")[:-3]
|
||||
elif model.lower().startswith("hive/"):
|
||||
model = "anthropic/" + model[len("hive/"):]
|
||||
model = "anthropic/" + model[len("hive/") :]
|
||||
if api_base and api_base.rstrip("/").endswith("/v1"):
|
||||
api_base = api_base.rstrip("/")[:-3]
|
||||
self.model = model
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
"label": "Kimi K2.5 - Best coding",
|
||||
"recommended": true,
|
||||
"max_tokens": 32768,
|
||||
"max_context_tokens": 240000
|
||||
"max_context_tokens": 200000
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -39,7 +39,7 @@ packages = ["framework"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 100
|
||||
line-length = 120
|
||||
|
||||
lint.select = [
|
||||
"B", # bugbear errors
|
||||
|
||||
@@ -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
@@ -2,4 +2,4 @@
|
||||
members = ["core", "tools"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
line-length = 120
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -92,7 +92,7 @@ packages = ["src/aden_tools"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 100
|
||||
line-length = 120
|
||||
|
||||
lint.select = [
|
||||
"B", # bugbear errors
|
||||
|
||||
@@ -513,7 +513,9 @@ class BeelineBridge:
|
||||
# Check if the element might be inside a Shadow DOM container
|
||||
shadow_hint = ""
|
||||
try:
|
||||
shadow_check = await self.evaluate(tab_id, """
|
||||
shadow_check = await self.evaluate(
|
||||
tab_id,
|
||||
"""
|
||||
(function() {
|
||||
var hosts = document.querySelectorAll('[id]');
|
||||
for (var h of hosts) {
|
||||
@@ -521,7 +523,8 @@ class BeelineBridge:
|
||||
}
|
||||
return null;
|
||||
})()
|
||||
""")
|
||||
""",
|
||||
)
|
||||
shadow_host = (shadow_check or {}).get("result")
|
||||
if shadow_host:
|
||||
shadow_hint = (
|
||||
@@ -1083,7 +1086,7 @@ class BeelineBridge:
|
||||
var box = document.createElement('div');
|
||||
box.id = '__hive_hl';
|
||||
box.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
|
||||
+ 'left:{int(x)}px;top:{int(y)}px;width:{max(1,int(w))}px;height:{max(1,int(h))}px;'
|
||||
+ 'left:{int(x)}px;top:{int(y)}px;width:{max(1, int(w))}px;height:{max(1, int(h))}px;'
|
||||
+ 'border:2px solid {border_rgb};background:{bg_rgba};'
|
||||
+ 'border-radius:3px;transition:opacity 0.4s ease;opacity:1;'
|
||||
+ 'box-shadow:0 0 8px {bg_rgba};';
|
||||
@@ -1111,8 +1114,12 @@ class BeelineBridge:
|
||||
pass # best-effort visual feedback
|
||||
|
||||
_interaction_highlights[tab_id] = {
|
||||
"x": x, "y": y, "w": w, "h": h,
|
||||
"label": label, "kind": "rect",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"w": w,
|
||||
"h": h,
|
||||
"label": label,
|
||||
"kind": "rect",
|
||||
}
|
||||
|
||||
async def highlight_point(self, tab_id: int, x: float, y: float, label: str = "") -> None:
|
||||
@@ -1128,7 +1135,7 @@ class BeelineBridge:
|
||||
var dot = document.createElement('div');
|
||||
dot.id = '__hive_hl';
|
||||
dot.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
|
||||
+ 'left:{int(x)-8}px;top:{int(y)-8}px;width:16px;height:16px;'
|
||||
+ 'left:{int(x) - 8}px;top:{int(y) - 8}px;width:16px;height:16px;'
|
||||
+ 'border-radius:50%;background:rgba(239,68,68,0.7);'
|
||||
+ 'box-shadow:0 0 0 4px rgba(239,68,68,0.25),0 0 12px rgba(239,68,68,0.4);'
|
||||
+ 'transition:opacity 0.4s ease;opacity:1;';
|
||||
@@ -1155,17 +1162,24 @@ class BeelineBridge:
|
||||
pass
|
||||
|
||||
_interaction_highlights[tab_id] = {
|
||||
"x": x, "y": y, "w": 0, "h": 0,
|
||||
"label": label, "kind": "point",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"w": 0,
|
||||
"h": 0,
|
||||
"label": label,
|
||||
"kind": "point",
|
||||
}
|
||||
|
||||
async def clear_highlight(self, tab_id: int) -> None:
|
||||
"""Remove the injected highlight from the page."""
|
||||
try:
|
||||
await self.evaluate(tab_id, """
|
||||
await self.evaluate(
|
||||
tab_id,
|
||||
"""
|
||||
var el = document.getElementById('__hive_hl');
|
||||
if (el) el.remove();
|
||||
""")
|
||||
""",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_interaction_highlights.pop(tab_id, None)
|
||||
|
||||
Reference in New Issue
Block a user