chore: ruff lint
This commit is contained in:
@@ -21,6 +21,7 @@ def __getattr__(name: str):
|
|||||||
LoopConfig,
|
LoopConfig,
|
||||||
OutputAccumulator,
|
OutputAccumulator,
|
||||||
)
|
)
|
||||||
|
|
||||||
_exports = {
|
_exports = {
|
||||||
"AgentLoop": AgentLoop,
|
"AgentLoop": AgentLoop,
|
||||||
"JudgeProtocol": JudgeProtocol,
|
"JudgeProtocol": JudgeProtocol,
|
||||||
|
|||||||
@@ -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",
|
{
|
||||||
"relationship",
|
"think",
|
||||||
"context",
|
"relationship",
|
||||||
"sentiment",
|
"context",
|
||||||
"physical_state",
|
"sentiment",
|
||||||
"tone",
|
"physical_state",
|
||||||
})
|
"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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
{
|
||||||
"name": data.get("name", ""),
|
"id": queen_id,
|
||||||
"title": data.get("title", ""),
|
"name": data.get("name", ""),
|
||||||
})
|
"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)
|
||||||
@@ -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
|
# Find the first '{' and last '}' to extract the JSON object
|
||||||
start = raw.find("{")
|
start = raw.find("{")
|
||||||
end = raw.rfind("}")
|
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:
|
try:
|
||||||
parsed = json.loads(json_str)
|
parsed = json.loads(json_str)
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -803,17 +794,16 @@ class AgentHost:
|
|||||||
|
|
||||||
# Register primary graph
|
# Register primary graph
|
||||||
self._graphs[self._graph_id] = _GraphRegistration(
|
self._graphs[self._graph_id] = _GraphRegistration(
|
||||||
graph=self.graph,
|
graph=self.graph,
|
||||||
goal=self.goal,
|
goal=self.goal,
|
||||||
entry_points=dict(self._entry_points),
|
entry_points=dict(self._entry_points),
|
||||||
streams=dict(self._streams),
|
streams=dict(self._streams),
|
||||||
storage_subpath="",
|
storage_subpath="",
|
||||||
event_subscriptions=list(self._event_subscriptions),
|
event_subscriptions=list(self._event_subscriptions),
|
||||||
timer_tasks=list(self._timer_tasks),
|
timer_tasks=list(self._timer_tasks),
|
||||||
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 ===
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -756,11 +758,11 @@ class LiteLLMProvider(LLMProvider):
|
|||||||
if _is_ollama_model(model):
|
if _is_ollama_model(model):
|
||||||
model = _ensure_ollama_chat_prefix(model)
|
model = _ensure_ollama_chat_prefix(model)
|
||||||
elif model.lower().startswith("kimi/"):
|
elif model.lower().startswith("kimi/"):
|
||||||
model = "anthropic/" + model[len("kimi/"):]
|
model = "anthropic/" + model[len("kimi/") :]
|
||||||
if api_base and api_base.rstrip("/").endswith("/v1"):
|
if api_base and api_base.rstrip("/").endswith("/v1"):
|
||||||
api_base = api_base.rstrip("/")[:-3]
|
api_base = api_base.rstrip("/")[:-3]
|
||||||
elif model.lower().startswith("hive/"):
|
elif model.lower().startswith("hive/"):
|
||||||
model = "anthropic/" + model[len("hive/"):]
|
model = "anthropic/" + model[len("hive/") :]
|
||||||
if api_base and api_base.rstrip("/").endswith("/v1"):
|
if api_base and api_base.rstrip("/").endswith("/v1"):
|
||||||
api_base = api_base.rstrip("/")[:-3]
|
api_base = api_base.rstrip("/")[:-3]
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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]]:
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ def _get_allowed_agent_roots() -> tuple[Path, ...]:
|
|||||||
from framework.config import COLONIES_DIR
|
from framework.config import COLONIES_DIR
|
||||||
|
|
||||||
_ALLOWED_AGENT_ROOTS = (
|
_ALLOWED_AGENT_ROOTS = (
|
||||||
COLONIES_DIR.resolve(), # ~/.hive/colonies/
|
COLONIES_DIR.resolve(), # ~/.hive/colonies/
|
||||||
(_REPO_ROOT / "exports").resolve(), # compat fallback
|
(_REPO_ROOT / "exports").resolve(), # compat fallback
|
||||||
(_REPO_ROOT / "examples").resolve(),
|
(_REPO_ROOT / "examples").resolve(),
|
||||||
(Path.home() / ".hive" / "agents").resolve(),
|
(Path.home() / ".hive" / "agents").resolve(),
|
||||||
)
|
)
|
||||||
@@ -244,23 +244,23 @@ def create_app(model: str | None = None) -> web.Application:
|
|||||||
credential_store = CredentialStore.for_testing({})
|
credential_store = CredentialStore.for_testing({})
|
||||||
|
|
||||||
app["credential_store"] = credential_store
|
app["credential_store"] = credential_store
|
||||||
|
|
||||||
# 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:
|
||||||
_queen_tool_registry = ToolRegistry()
|
_queen_tool_registry = ToolRegistry()
|
||||||
import framework.agents.queen as _queen_pkg
|
import framework.agents.queen as _queen_pkg
|
||||||
|
|
||||||
queen_pkg_dir = Path(_queen_pkg.__file__).parent
|
queen_pkg_dir = Path(_queen_pkg.__file__).parent
|
||||||
mcp_config = queen_pkg_dir / "mcp_servers.json"
|
mcp_config = queen_pkg_dir / "mcp_servers.json"
|
||||||
if mcp_config.exists():
|
if mcp_config.exists():
|
||||||
_queen_tool_registry.load_mcp_config(mcp_config)
|
_queen_tool_registry.load_mcp_config(mcp_config)
|
||||||
logger.info("Pre-loaded queen MCP tools from %s", mcp_config)
|
logger.info("Pre-loaded queen MCP tools from %s", mcp_config)
|
||||||
|
|
||||||
registry = MCPRegistry()
|
registry = MCPRegistry()
|
||||||
registry.initialize()
|
registry.initialize()
|
||||||
if (queen_pkg_dir / "mcp_registry.json").is_file():
|
if (queen_pkg_dir / "mcp_registry.json").is_file():
|
||||||
@@ -273,12 +273,16 @@ def create_app(model: str | None = None) -> web.Application:
|
|||||||
log_collisions=True,
|
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)
|
||||||
|
|||||||
@@ -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,15 +105,17 @@ 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"],
|
{
|
||||||
"name": definition["name"],
|
"id": definition["id"],
|
||||||
"description": definition["description"],
|
"name": definition["name"],
|
||||||
"provider": preset["provider"],
|
"description": definition["description"],
|
||||||
"flag": definition["flag"],
|
"provider": preset["provider"],
|
||||||
"default_model": preset.get("model", ""),
|
"flag": definition["flag"],
|
||||||
**({"api_base": preset["api_base"]} if preset.get("api_base") else {}),
|
"default_model": preset.get("model", ""),
|
||||||
})
|
**({"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,17 +324,19 @@ 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,
|
{
|
||||||
"model": model,
|
"provider": provider,
|
||||||
"has_api_key": has_key,
|
"model": model,
|
||||||
"max_tokens": llm.get("max_tokens"),
|
"has_api_key": has_key,
|
||||||
"max_context_tokens": llm.get("max_context_tokens"),
|
"max_tokens": llm.get("max_tokens"),
|
||||||
"connected_providers": connected,
|
"max_context_tokens": llm.get("max_context_tokens"),
|
||||||
"active_subscription": active_subscription,
|
"connected_providers": connected,
|
||||||
"detected_subscriptions": detected_subscriptions,
|
"active_subscription": active_subscription,
|
||||||
"subscriptions": SUBSCRIPTIONS,
|
"detected_subscriptions": detected_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,18 +404,22 @@ 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,
|
{
|
||||||
"model": model,
|
"provider": provider,
|
||||||
"has_api_key": token is not None,
|
"model": model,
|
||||||
"max_tokens": max_tokens,
|
"has_api_key": token is not None,
|
||||||
"max_context_tokens": max_context_tokens,
|
"max_tokens": max_tokens,
|
||||||
"sessions_swapped": swapped,
|
"max_context_tokens": max_context_tokens,
|
||||||
"active_subscription": subscription_id,
|
"sessions_swapped": swapped,
|
||||||
})
|
"active_subscription": subscription_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# ── API key mode ─────────────────────────────────────────────
|
# ── API key mode ─────────────────────────────────────────────
|
||||||
@@ -450,46 +465,52 @@ 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,
|
{
|
||||||
"model": model,
|
"provider": provider,
|
||||||
"has_api_key": api_key is not None,
|
"model": model,
|
||||||
"max_tokens": max_tokens,
|
"has_api_key": api_key is not None,
|
||||||
"max_context_tokens": max_context_tokens,
|
"max_tokens": max_tokens,
|
||||||
"sessions_swapped": swapped,
|
"max_context_tokens": max_context_tokens,
|
||||||
"active_subscription": None,
|
"sessions_swapped": swapped,
|
||||||
})
|
"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", ""),
|
{
|
||||||
"about": profile.get("about", ""),
|
"displayName": profile.get("displayName", ""),
|
||||||
"theme": profile.get("theme", ""),
|
"about": profile.get("about", ""),
|
||||||
})
|
"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:
|
||||||
"""Sync user profile to global memory as a profile-type memory file.
|
"""Sync user profile to global memory as a profile-type memory file.
|
||||||
|
|
||||||
Uses the canonical filename 'user-profile.md' — this is the single
|
Uses the canonical filename 'user-profile.md' — this is the single
|
||||||
source of truth for user identity information, shared with the
|
source of truth for user identity information, shared with the
|
||||||
reflection agent.
|
reflection agent.
|
||||||
|
|
||||||
Merges with existing content to preserve sections added by the reflection agent.
|
Merges with existing content to preserve sections added by the reflection agent.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
mem_dir = global_memory_dir()
|
mem_dir = global_memory_dir()
|
||||||
mem_dir.mkdir(parents=True, exist_ok=True)
|
mem_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
profile_filename = "user-profile.md"
|
profile_filename = "user-profile.md"
|
||||||
memory_path = mem_dir / profile_filename
|
memory_path = mem_dir / profile_filename
|
||||||
|
|
||||||
# Read existing content if present
|
# Read existing content if present
|
||||||
existing_body = ""
|
existing_body = ""
|
||||||
if memory_path.exists():
|
if memory_path.exists():
|
||||||
@@ -499,16 +520,16 @@ def _update_user_profile_memory(display_name: str, about: str) -> None:
|
|||||||
parts = existing_text.split("---\n", 2)
|
parts = existing_text.split("---\n", 2)
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
existing_body = parts[2].strip()
|
existing_body = parts[2].strip()
|
||||||
|
|
||||||
# Build Identity section from settings
|
# Build Identity section from settings
|
||||||
identity_lines = []
|
identity_lines = []
|
||||||
if display_name:
|
if display_name:
|
||||||
identity_lines.append(f"- **Name:** {display_name}")
|
identity_lines.append(f"- **Name:** {display_name}")
|
||||||
if about:
|
if about:
|
||||||
identity_lines.append(f"- **About:** {about}")
|
identity_lines.append(f"- **About:** {about}")
|
||||||
|
|
||||||
identity_section = "## Identity\n" + "\n".join(identity_lines) if identity_lines else ""
|
identity_section = "## Identity\n" + "\n".join(identity_lines) if identity_lines else ""
|
||||||
|
|
||||||
# Merge: replace or prepend Identity section, keep rest
|
# Merge: replace or prepend Identity section, keep rest
|
||||||
if existing_body and "## Identity" in existing_body:
|
if existing_body and "## Identity" in existing_body:
|
||||||
# Replace existing Identity section
|
# Replace existing Identity section
|
||||||
@@ -522,14 +543,16 @@ def _update_user_profile_memory(display_name: str, about: str) -> None:
|
|||||||
else:
|
else:
|
||||||
# Just Identity section
|
# Just Identity section
|
||||||
new_body = identity_section
|
new_body = identity_section
|
||||||
|
|
||||||
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.",
|
||||||
)
|
)
|
||||||
|
|
||||||
memory_path.write_text(content, encoding="utf-8")
|
memory_path.write_text(content, encoding="utf-8")
|
||||||
logger.debug("User profile synced to global memory: %s", memory_path)
|
logger.debug("User profile synced to global memory: %s", memory_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -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", ""),
|
{
|
||||||
"about": profile.get("about", ""),
|
"displayName": profile.get("displayName", ""),
|
||||||
"theme": profile.get("theme", ""),
|
"about": profile.get("about", ""),
|
||||||
})
|
"theme": profile.get("theme", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_get_models(request: web.Request) -> web.Response:
|
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:
|
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,37 +243,42 @@ 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_id": cred_id,
|
"credential_name": name,
|
||||||
"env_var": spec.env_var,
|
"credential_id": cred_id,
|
||||||
"description": spec.description,
|
"env_var": spec.env_var,
|
||||||
"help_url": spec.help_url,
|
"description": spec.description,
|
||||||
"api_key_instructions": spec.api_key_instructions,
|
"help_url": spec.help_url,
|
||||||
"tools": spec.tools,
|
"api_key_instructions": spec.api_key_instructions,
|
||||||
"aden_supported": spec.aden_supported,
|
"tools": spec.tools,
|
||||||
"direct_api_key_supported": spec.direct_api_key_supported,
|
"aden_supported": spec.aden_supported,
|
||||||
"credential_key": spec.credential_key,
|
"direct_api_key_supported": spec.direct_api_key_supported,
|
||||||
"credential_group": spec.credential_group,
|
"credential_key": spec.credential_key,
|
||||||
"available": store.is_available(cred_id),
|
"credential_group": spec.credential_group,
|
||||||
})
|
"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(
|
||||||
"credential_name": "Aden Platform",
|
0,
|
||||||
"credential_id": "aden_api_key",
|
{
|
||||||
"env_var": "ADEN_API_KEY",
|
"credential_name": "Aden Platform",
|
||||||
"description": "API key from the Developers tab in Settings",
|
"credential_id": "aden_api_key",
|
||||||
"help_url": "https://hive.adenhq.com/",
|
"env_var": "ADEN_API_KEY",
|
||||||
"api_key_instructions": "1. Go to hive.adenhq.com\n2. Open Settings > Developers\n3. Copy your API key",
|
"description": "API key from the Developers tab in Settings",
|
||||||
"tools": [],
|
"help_url": "https://hive.adenhq.com/",
|
||||||
"aden_supported": True,
|
"api_key_instructions": "1. Go to hive.adenhq.com\n2. Open Settings > Developers\n3. Copy your API key",
|
||||||
"direct_api_key_supported": True,
|
"tools": [],
|
||||||
"credential_key": "api_key",
|
"aden_supported": True,
|
||||||
"credential_group": "",
|
"direct_api_key_supported": True,
|
||||||
"available": has_aden_key,
|
"credential_key": "api_key",
|
||||||
})
|
"credential_group": "",
|
||||||
|
"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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,22 +20,22 @@ async def handle_new_message(request: web.Request) -> web.Response:
|
|||||||
message = message.strip()
|
message = message.strip()
|
||||||
|
|
||||||
ensure_default_queens()
|
ensure_default_queens()
|
||||||
|
|
||||||
# Build LLM for classification
|
# Build LLM for classification
|
||||||
llm = manager.build_llm()
|
llm = manager.build_llm()
|
||||||
|
|
||||||
# Run queen selection - this is the slow part we can't avoid
|
# Run queen selection - this is the slow part we can't avoid
|
||||||
queen_id = await select_queen(message, llm)
|
queen_id = await select_queen(message, llm)
|
||||||
|
|
||||||
await _stop_live_sessions(manager)
|
await _stop_live_sessions(manager)
|
||||||
|
|
||||||
# Create session with pre-bound queen
|
# Create session with pre-bound queen
|
||||||
session = await manager.create_session(
|
session = await manager.create_session(
|
||||||
initial_prompt=message,
|
initial_prompt=message,
|
||||||
queen_name=queen_id,
|
queen_name=queen_id,
|
||||||
initial_phase="independent",
|
initial_phase="independent",
|
||||||
)
|
)
|
||||||
|
|
||||||
await session.event_bus.publish(
|
await session.event_bus.publish(
|
||||||
AgentEvent(
|
AgentEvent(
|
||||||
type=EventType.CLIENT_INPUT_RECEIVED,
|
type=EventType.CLIENT_INPUT_RECEIVED,
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ async def handle_list_profiles(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
def _transform_profile_for_api(profile: dict) -> dict:
|
def _transform_profile_for_api(profile: dict) -> dict:
|
||||||
"""Transform internal profile format to API format expected by frontend.
|
"""Transform internal profile format to API format expected by frontend.
|
||||||
|
|
||||||
Maps YAML fields (core_traits, hidden_background, etc.) to display fields
|
Maps YAML fields (core_traits, hidden_background, etc.) to display fields
|
||||||
(summary, experience, skills, signature_achievement).
|
(summary, experience, skills, signature_achievement).
|
||||||
"""
|
"""
|
||||||
@@ -127,7 +127,7 @@ def _transform_profile_for_api(profile: dict) -> dict:
|
|||||||
"name": profile.get("name", ""),
|
"name": profile.get("name", ""),
|
||||||
"title": profile.get("title", ""),
|
"title": profile.get("title", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build summary from core_traits + psychological_profile
|
# Build summary from core_traits + psychological_profile
|
||||||
summary_parts = []
|
summary_parts = []
|
||||||
if profile.get("core_traits"):
|
if profile.get("core_traits"):
|
||||||
@@ -136,7 +136,7 @@ def _transform_profile_for_api(profile: dict) -> dict:
|
|||||||
summary_parts.append(profile["psychological_profile"]["anti_stereotype"])
|
summary_parts.append(profile["psychological_profile"]["anti_stereotype"])
|
||||||
if summary_parts:
|
if summary_parts:
|
||||||
result["summary"] = "\n\n".join(summary_parts)
|
result["summary"] = "\n\n".join(summary_parts)
|
||||||
|
|
||||||
# Build experience from hidden_background
|
# Build experience from hidden_background
|
||||||
experience = []
|
experience = []
|
||||||
hidden = profile.get("hidden_background", {})
|
hidden = profile.get("hidden_background", {})
|
||||||
@@ -148,22 +148,23 @@ 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
|
||||||
|
|
||||||
# Skills from skills field
|
# Skills from skills field
|
||||||
if profile.get("skills"):
|
if profile.get("skills"):
|
||||||
result["skills"] = profile["skills"]
|
result["skills"] = profile["skills"]
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ async def handle_get_profile(request: web.Request) -> web.Response:
|
|||||||
profile = load_queen_profile(queen_id)
|
profile = load_queen_profile(queen_id)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return web.json_response({"error": f"Queen '{queen_id}' not found"}, status=404)
|
return web.json_response({"error": f"Queen '{queen_id}' not found"}, status=404)
|
||||||
|
|
||||||
api_profile = _transform_profile_for_api(profile)
|
api_profile = _transform_profile_for_api(profile)
|
||||||
return web.json_response({"id": queen_id, **api_profile})
|
return web.json_response({"id": queen_id, **api_profile})
|
||||||
|
|
||||||
@@ -224,11 +225,13 @@ async def handle_queen_session(request: web.Request) -> web.Response:
|
|||||||
# 1. Check for an existing live session bound to this queen.
|
# 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,
|
{
|
||||||
"queen_id": queen_id,
|
"session_id": session.id,
|
||||||
"status": "live",
|
"queen_id": queen_id,
|
||||||
})
|
"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,
|
{
|
||||||
"queen_id": queen_id,
|
"session_id": session.id,
|
||||||
"status": status,
|
"queen_id": queen_id,
|
||||||
})
|
"status": status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_select_queen_session(request: web.Request) -> web.Response:
|
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
|
# 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
project_root=self._config.project_root,
|
DiscoveryConfig(
|
||||||
skip_framework_scope=False,
|
project_root=self._config.project_root,
|
||||||
))
|
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -2,4 +2,4 @@
|
|||||||
members = ["core", "tools"]
|
members = ["core", "tools"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 120
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = (
|
||||||
@@ -1083,7 +1086,7 @@ class BeelineBridge:
|
|||||||
var box = document.createElement('div');
|
var box = document.createElement('div');
|
||||||
box.id = '__hive_hl';
|
box.id = '__hive_hl';
|
||||||
box.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
|
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:2px solid {border_rgb};background:{bg_rgba};'
|
||||||
+ 'border-radius:3px;transition:opacity 0.4s ease;opacity:1;'
|
+ 'border-radius:3px;transition:opacity 0.4s ease;opacity:1;'
|
||||||
+ 'box-shadow:0 0 8px {bg_rgba};';
|
+ 'box-shadow:0 0 8px {bg_rgba};';
|
||||||
@@ -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:
|
||||||
@@ -1128,7 +1135,7 @@ class BeelineBridge:
|
|||||||
var dot = document.createElement('div');
|
var dot = document.createElement('div');
|
||||||
dot.id = '__hive_hl';
|
dot.id = '__hive_hl';
|
||||||
dot.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
|
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);'
|
+ '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);'
|
+ '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;';
|
+ 'transition:opacity 0.4s ease;opacity:1;';
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user