Merge branch 'feat/queen-profile' into feature/hive-experimental-comp-pipeline
This commit is contained in:
@@ -52,28 +52,33 @@ def _get_last_active(agent_path: Path) -> str | None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2. Queen sessions
|
# 2. Queen sessions (scan all queen identity directories)
|
||||||
from framework.config import QUEENS_DIR
|
from framework.config import QUEENS_DIR
|
||||||
|
|
||||||
queen_sessions_dir = QUEENS_DIR / "default" / "sessions"
|
if QUEENS_DIR.exists():
|
||||||
if queen_sessions_dir.exists():
|
|
||||||
resolved = agent_path.resolve()
|
resolved = agent_path.resolve()
|
||||||
for d in queen_sessions_dir.iterdir():
|
for queen_dir in QUEENS_DIR.iterdir():
|
||||||
if not d.is_dir():
|
if not queen_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
meta_file = d / "meta.json"
|
sessions_dir = queen_dir / "sessions"
|
||||||
if not meta_file.exists():
|
if not sessions_dir.exists():
|
||||||
continue
|
continue
|
||||||
try:
|
for d in sessions_dir.iterdir():
|
||||||
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
if not d.is_dir():
|
||||||
stored = meta.get("agent_path")
|
continue
|
||||||
if not stored or Path(stored).resolve() != resolved:
|
meta_file = d / "meta.json"
|
||||||
|
if not meta_file.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
||||||
|
stored = meta.get("agent_path")
|
||||||
|
if not stored or Path(stored).resolve() != resolved:
|
||||||
|
continue
|
||||||
|
ts = datetime.fromtimestamp(d.stat().st_mtime).isoformat()
|
||||||
|
if latest is None or ts > latest:
|
||||||
|
latest = ts
|
||||||
|
except Exception:
|
||||||
continue
|
continue
|
||||||
ts = datetime.fromtimestamp(d.stat().st_mtime).isoformat()
|
|
||||||
if latest is None or ts > latest:
|
|
||||||
latest = ts
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return latest
|
return latest
|
||||||
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
"""Queen thinking hook — persona + communication style classifier.
|
|
||||||
|
|
||||||
Fires once when the queen enters building mode at session start.
|
|
||||||
Makes a single non-streaming LLM call (acting as an HR Director) to select
|
|
||||||
the best-fit expert persona for the user's request AND classify the user's
|
|
||||||
communication style, then returns a PersonaResult containing both.
|
|
||||||
|
|
||||||
This is designed to activate the model's latent domain expertise — a CFO
|
|
||||||
persona on a financial question, a Lawyer on a legal question, etc. — while
|
|
||||||
also adapting the Queen's communication approach to the individual user.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from framework.llm.provider import LLMProvider
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_HR_SYSTEM_PROMPT = """\
|
|
||||||
You are an expert HR Director and communication consultant at a world-class firm.
|
|
||||||
A new request has arrived. You must:
|
|
||||||
1. Identify which professional role best serves this request.
|
|
||||||
2. Read the user's signals to determine HOW to communicate with them.
|
|
||||||
|
|
||||||
For communication style, look for:
|
|
||||||
- Technical depth: Do they use precise terms? Do they ask "how" or "what"?
|
|
||||||
- Pace: Short messages = fast and direct. Long explanations = exploratory.
|
|
||||||
- Tone: Are they casual ("hey, can you...") or formal ("I need a system that...")?
|
|
||||||
|
|
||||||
If cross-session memory is provided, factor in what is already known about this \
|
|
||||||
person — don't rediscover what's already understood.
|
|
||||||
|
|
||||||
Reply with ONLY a valid JSON object — no markdown, no prose, no explanation:
|
|
||||||
{"role": "<job title>", "persona": "<2-3 sentence first-person identity statement>", \
|
|
||||||
"style": "<one of: peer-technical, mentor-guiding, consultant-structured>"}
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Choose from any real professional role: CFO, CEO, CTO, Lawyer, Data Scientist,
|
|
||||||
Product Manager, Security Engineer, DevOps Engineer, Software Architect,
|
|
||||||
HR Director, Marketing Director, Business Analyst, UX Designer,
|
|
||||||
Financial Analyst, Operations Director, Legal Counsel, etc.
|
|
||||||
- The persona statement must be written in first person ("I am..." or "I have...").
|
|
||||||
- Select the role whose domain knowledge most directly applies to solving the request.
|
|
||||||
- If the request is clearly about coding or building software systems, pick Software Architect.
|
|
||||||
- "Queen" is your internal alias — do not include it in the persona.
|
|
||||||
- For style: "peer-technical" for users who demonstrate domain expertise, \
|
|
||||||
"mentor-guiding" for users who are learning or exploring, \
|
|
||||||
"consultant-structured" for users who want structured, accountable delivery.
|
|
||||||
- Default to "peer-technical" if signals are ambiguous.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Communication style directives injected into the Queen's system prompt.
|
|
||||||
_STYLE_DIRECTIVES: dict[str, str] = {
|
|
||||||
"peer-technical": (
|
|
||||||
"## Communication Style: Peer\n\n"
|
|
||||||
"This person is technical. Use precise language, skip high-level "
|
|
||||||
"overviews they already know, and get into specifics quickly. "
|
|
||||||
"When they push back on a design choice, engage with the technical "
|
|
||||||
"argument directly."
|
|
||||||
),
|
|
||||||
"mentor-guiding": (
|
|
||||||
"## Communication Style: Guide\n\n"
|
|
||||||
"This person is learning or exploring. Explain your reasoning as you "
|
|
||||||
"go — not patronizingly, but so they can follow the logic. When you "
|
|
||||||
"make a design choice, briefly say why. Offer to go deeper on anything."
|
|
||||||
),
|
|
||||||
"consultant-structured": (
|
|
||||||
"## Communication Style: Structured\n\n"
|
|
||||||
"This person wants structured, accountable delivery. Lead with "
|
|
||||||
"summaries and options. Number your proposals. Be explicit about "
|
|
||||||
"trade-offs. Avoid open-ended questions — give them choices to react to."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PersonaResult:
|
|
||||||
"""Result of persona + style classification."""
|
|
||||||
|
|
||||||
persona_prefix: str # e.g. "You are a CFO. I am a CFO with 20 years..."
|
|
||||||
style_directive: str # e.g. "## Communication Style: Peer\n\n..."
|
|
||||||
|
|
||||||
|
|
||||||
async def select_expert_persona(
|
|
||||||
user_message: str,
|
|
||||||
llm: LLMProvider,
|
|
||||||
*,
|
|
||||||
memory_context: str = "",
|
|
||||||
) -> PersonaResult | None:
|
|
||||||
"""Run the HR classifier and return a PersonaResult.
|
|
||||||
|
|
||||||
Makes a single non-streaming acomplete() call with the session LLM.
|
|
||||||
Returns None on any failure so the queen falls back gracefully to its
|
|
||||||
default character with no style directive.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_message: The user's opening message for the session.
|
|
||||||
llm: The session LLM provider.
|
|
||||||
memory_context: Optional cross-session memory to inform style classification.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A PersonaResult with persona_prefix and style_directive, or None on failure.
|
|
||||||
"""
|
|
||||||
if not user_message.strip():
|
|
||||||
return None
|
|
||||||
|
|
||||||
prompt = user_message
|
|
||||||
if memory_context:
|
|
||||||
prompt = f"{user_message}\n\n{memory_context}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await llm.acomplete(
|
|
||||||
messages=[{"role": "user", "content": prompt}],
|
|
||||||
system=_HR_SYSTEM_PROMPT,
|
|
||||||
max_tokens=1024,
|
|
||||||
json_mode=True,
|
|
||||||
)
|
|
||||||
raw = response.content.strip()
|
|
||||||
parsed = json.loads(raw)
|
|
||||||
role = parsed.get("role", "").strip()
|
|
||||||
persona = parsed.get("persona", "").strip()
|
|
||||||
style_key = parsed.get("style", "peer-technical").strip()
|
|
||||||
if not role or not persona:
|
|
||||||
logger.warning("Thinking hook: empty role/persona in response: %r", raw)
|
|
||||||
return None
|
|
||||||
persona_prefix = f"You are a {role}. {persona}"
|
|
||||||
style_directive = _STYLE_DIRECTIVES.get(style_key, _STYLE_DIRECTIVES["peer-technical"])
|
|
||||||
logger.info("Thinking hook: selected persona — %s, style — %s", role, style_key)
|
|
||||||
return PersonaResult(persona_prefix=persona_prefix, style_directive=style_directive)
|
|
||||||
except Exception:
|
|
||||||
logger.warning("Thinking hook: persona classification failed", exc_info=True)
|
|
||||||
return None
|
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
"""Queen identity profiles -- static queen personas stored as YAML files.
|
||||||
|
|
||||||
|
Each queen has a unique identity (Head of Technology, Head of Growth, etc.)
|
||||||
|
stored in ``~/.hive/agents/queens/{queen_id}/profile.yaml``. Profiles are
|
||||||
|
initialized with rich defaults and can be edited via the API.
|
||||||
|
|
||||||
|
At session start, a lightweight LLM classifier selects the best-matching
|
||||||
|
queen for the user's request, and the profile is injected into the system
|
||||||
|
prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from framework.config import QUEENS_DIR
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from framework.llm.provider import LLMProvider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Default queen profiles
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DEFAULT_QUEENS: dict[str, dict[str, Any]] = {
|
||||||
|
"queen_technology": {
|
||||||
|
"name": "Alexandra",
|
||||||
|
"title": "Head of Technology",
|
||||||
|
"summary": (
|
||||||
|
"Technical leader with 12+ years building scalable systems for "
|
||||||
|
"startups from 0 to $50M ARR. Expert in translating business ideas "
|
||||||
|
"into robust, cost-efficient architectures."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Technology — Multiple Startups (2020–Present)",
|
||||||
|
"details": [
|
||||||
|
"Led architecture for 5 startups (2 successful exits)",
|
||||||
|
"Reduced cloud costs by 40% through infrastructure redesign",
|
||||||
|
"Hired and managed teams of 10–25 engineers",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "VP Engineering — SaaSCo (2017–2020)",
|
||||||
|
"details": [
|
||||||
|
"Scaled platform to 1M+ users",
|
||||||
|
"Migrated monolith to microservices",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "System design, cloud infrastructure, hiring, DevOps, scalability",
|
||||||
|
"signature_achievement": "Built MVP and scaled to 500k users without major rewrite",
|
||||||
|
},
|
||||||
|
"queen_growth": {
|
||||||
|
"name": "Marcus",
|
||||||
|
"title": "Head of Growth",
|
||||||
|
"summary": (
|
||||||
|
"Growth strategist who has taken three B2B SaaS products from "
|
||||||
|
"launch to $10M+ ARR. Deep expertise in acquisition funnels, "
|
||||||
|
"retention loops, and data-driven experimentation."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Growth — ScaleUp Inc. (2021–Present)",
|
||||||
|
"details": [
|
||||||
|
"Grew MRR from $200k to $2.5M in 18 months",
|
||||||
|
"Built growth team of 8 across acquisition, activation, and retention",
|
||||||
|
"Designed referral program generating 30% of new signups",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Senior Growth Manager — RapidLaunch (2018–2021)",
|
||||||
|
"details": [
|
||||||
|
"Led product-led growth strategy reaching 500k free users",
|
||||||
|
"Reduced CAC by 55% through organic channel optimization",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Growth modeling, A/B testing, funnel optimization, PLG strategy, analytics",
|
||||||
|
"signature_achievement": "Built self-serve acquisition engine that drove 70% of revenue with zero sales team",
|
||||||
|
},
|
||||||
|
"queen_product_strategy": {
|
||||||
|
"name": "Sophia",
|
||||||
|
"title": "Head of Product Strategy",
|
||||||
|
"summary": (
|
||||||
|
"Product leader with a track record of defining and executing "
|
||||||
|
"product vision for early-stage startups. Bridges user research, "
|
||||||
|
"business strategy, and engineering to ship products people love."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Product — NovaTech (2020–Present)",
|
||||||
|
"details": [
|
||||||
|
"Defined product roadmap that drove 3x user growth in one year",
|
||||||
|
"Introduced OKR framework aligning product, engineering, and sales",
|
||||||
|
"Led discovery sprints that identified $5M untapped market segment",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Senior Product Manager — BuildFast (2017–2020)",
|
||||||
|
"details": [
|
||||||
|
"Shipped 12 major features with 95% on-time delivery",
|
||||||
|
"Grew NPS from 32 to 67 through systematic user feedback loops",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Product roadmapping, user research, prioritization frameworks, go-to-market strategy",
|
||||||
|
"signature_achievement": "Pivoted failing product into market leader within 9 months by redefining ICP and value proposition",
|
||||||
|
},
|
||||||
|
"queen_finance_fundraising": {
|
||||||
|
"name": "Daniel",
|
||||||
|
"title": "Head of Finance & Fundraising",
|
||||||
|
"summary": (
|
||||||
|
"Finance executive who has raised over $150M across seed to Series C "
|
||||||
|
"rounds. Expert in financial modeling, unit economics, and investor "
|
||||||
|
"relations for high-growth startups."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Finance — VentureScale (2019–Present)",
|
||||||
|
"details": [
|
||||||
|
"Led $45M Series B raise at 12x revenue multiple",
|
||||||
|
"Built financial planning infrastructure from scratch",
|
||||||
|
"Reduced burn rate by 25% while maintaining growth trajectory",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Finance Director — FinBridge Capital (2016–2019)",
|
||||||
|
"details": [
|
||||||
|
"Advised 20+ startups on fundraising strategy and cap table management",
|
||||||
|
"Structured convertible notes and SAFEs for early-stage companies",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Financial modeling, fundraising strategy, investor relations, cap table management, unit economics",
|
||||||
|
"signature_achievement": "Closed oversubscribed Series A in 3 weeks with 40+ inbound investor inquiries",
|
||||||
|
},
|
||||||
|
"queen_legal": {
|
||||||
|
"name": "Catherine",
|
||||||
|
"title": "Head of Legal",
|
||||||
|
"summary": (
|
||||||
|
"Startup legal counsel with deep expertise in corporate governance, "
|
||||||
|
"IP protection, and regulatory compliance. Has guided 15+ startups "
|
||||||
|
"through incorporations, funding rounds, and exits."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "General Counsel — TechLegal Partners (2019–Present)",
|
||||||
|
"details": [
|
||||||
|
"Structured legal frameworks for startups across 5 jurisdictions",
|
||||||
|
"Negotiated $200M+ in commercial contracts",
|
||||||
|
"Managed IP portfolio of 30+ patents and trademarks",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Corporate Attorney — Whitfield & Associates (2015–2019)",
|
||||||
|
"details": [
|
||||||
|
"Led due diligence for 12 M&A transactions",
|
||||||
|
"Drafted and negotiated term sheets for Series A through C rounds",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Corporate law, IP protection, contract negotiation, regulatory compliance, employment law",
|
||||||
|
"signature_achievement": "Saved client $3M by identifying and resolving IP ownership dispute before Series B close",
|
||||||
|
},
|
||||||
|
"queen_brand_design": {
|
||||||
|
"name": "Elena",
|
||||||
|
"title": "Head of Brand & Design",
|
||||||
|
"summary": (
|
||||||
|
"Creative director who builds brand identities that drive business "
|
||||||
|
"results. Expert in translating startup vision into cohesive visual "
|
||||||
|
"systems, messaging frameworks, and user experiences."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Brand & Design — StudioPulse (2020–Present)",
|
||||||
|
"details": [
|
||||||
|
"Built brand identity for 10+ funded startups from zero",
|
||||||
|
"Designed design systems adopted by engineering teams of 20+",
|
||||||
|
"Led rebrand that increased conversion rate by 35%",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Senior Design Lead — CreativeForge (2017–2020)",
|
||||||
|
"details": [
|
||||||
|
"Managed team of 6 designers across brand, product, and marketing",
|
||||||
|
"Established design ops practice reducing design-to-dev handoff time by 60%",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Brand strategy, visual identity, design systems, UX design, creative direction",
|
||||||
|
"signature_achievement": "Created brand identity for pre-launch startup that became recognizable industry name within 6 months",
|
||||||
|
},
|
||||||
|
"queen_talent": {
|
||||||
|
"name": "James",
|
||||||
|
"title": "Head of Talent",
|
||||||
|
"summary": (
|
||||||
|
"People leader who has built high-performing teams from founding "
|
||||||
|
"stage to 200+ employees. Expert in recruiting strategy, culture "
|
||||||
|
"building, and organizational design for fast-scaling startups."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Talent — HyperGrowth Labs (2020–Present)",
|
||||||
|
"details": [
|
||||||
|
"Scaled team from 15 to 180 in 18 months with 92% retention",
|
||||||
|
"Built recruiting engine processing 5,000+ candidates per quarter",
|
||||||
|
"Designed compensation framework competitive across 12 markets",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Senior Recruiter — TalentBridge (2017–2020)",
|
||||||
|
"details": [
|
||||||
|
"Placed 100+ engineering and leadership hires at Series A–C startups",
|
||||||
|
"Reduced average time-to-hire from 45 to 22 days",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Recruiting strategy, organizational design, culture building, compensation planning, employer branding",
|
||||||
|
"signature_achievement": "Built engineering team of 50 in 6 months with zero external agency spend",
|
||||||
|
},
|
||||||
|
"queen_operations": {
|
||||||
|
"name": "Rachel",
|
||||||
|
"title": "Head of Operations",
|
||||||
|
"summary": (
|
||||||
|
"Operations leader who builds the systems that let startups scale "
|
||||||
|
"without chaos. Expert in process design, vendor management, and "
|
||||||
|
"cross-functional coordination."
|
||||||
|
),
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"role": "Head of Operations — OptiFlow (2020–Present)",
|
||||||
|
"details": [
|
||||||
|
"Designed operational playbooks supporting 10x revenue growth",
|
||||||
|
"Managed $8M annual vendor budget with 20% cost reduction",
|
||||||
|
"Built cross-functional workflows connecting sales, product, and support",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Operations Manager — StreamLine Co. (2017–2020)",
|
||||||
|
"details": [
|
||||||
|
"Automated 40% of manual operational processes",
|
||||||
|
"Led office expansion across 3 new markets",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"skills": "Process optimization, vendor management, cross-functional coordination, project management, systems thinking",
|
||||||
|
"signature_achievement": "Built operational infrastructure that supported 5x team growth with zero additional ops hires",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Profile CRUD
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_default_queens() -> None:
|
||||||
|
"""Create default queen profiles on disk if they don't already exist.
|
||||||
|
|
||||||
|
Safe to call multiple times — skips any profile that already has a file.
|
||||||
|
"""
|
||||||
|
for queen_id, profile in DEFAULT_QUEENS.items():
|
||||||
|
queen_dir = QUEENS_DIR / queen_id
|
||||||
|
profile_path = queen_dir / "profile.yaml"
|
||||||
|
if profile_path.exists():
|
||||||
|
continue
|
||||||
|
queen_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
profile_path.write_text(yaml.safe_dump(profile, sort_keys=False, allow_unicode=True))
|
||||||
|
logger.info("Queen profiles ensured at %s", QUEENS_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def list_queens() -> list[dict[str, str]]:
|
||||||
|
"""Return a summary list of all queen profiles on disk."""
|
||||||
|
results: list[dict[str, str]] = []
|
||||||
|
if not QUEENS_DIR.is_dir():
|
||||||
|
return results
|
||||||
|
for profile_path in sorted(QUEENS_DIR.glob("*/profile.yaml")):
|
||||||
|
queen_id = profile_path.parent.name
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(profile_path.read_text())
|
||||||
|
results.append({
|
||||||
|
"id": queen_id,
|
||||||
|
"name": data.get("name", ""),
|
||||||
|
"title": data.get("title", ""),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to read queen profile %s", profile_path)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def load_queen_profile(queen_id: str) -> dict[str, Any]:
|
||||||
|
"""Load and return a queen's full profile.
|
||||||
|
|
||||||
|
Raises FileNotFoundError if the profile doesn't exist.
|
||||||
|
"""
|
||||||
|
profile_path = QUEENS_DIR / queen_id / "profile.yaml"
|
||||||
|
if not profile_path.exists():
|
||||||
|
raise FileNotFoundError(f"Queen profile not found: {queen_id}")
|
||||||
|
data = yaml.safe_load(profile_path.read_text())
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def update_queen_profile(queen_id: str, updates: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Merge partial updates into an existing queen profile and persist.
|
||||||
|
|
||||||
|
Returns the full updated profile.
|
||||||
|
Raises FileNotFoundError if the profile doesn't exist.
|
||||||
|
"""
|
||||||
|
profile_path = QUEENS_DIR / queen_id / "profile.yaml"
|
||||||
|
if not profile_path.exists():
|
||||||
|
raise FileNotFoundError(f"Queen profile not found: {queen_id}")
|
||||||
|
data = yaml.safe_load(profile_path.read_text())
|
||||||
|
data.update(updates)
|
||||||
|
profile_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Prompt formatting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def format_queen_identity_prompt(profile: dict[str, Any]) -> str:
|
||||||
|
"""Convert a queen profile dict into a system prompt identity section."""
|
||||||
|
parts = [
|
||||||
|
f"# Your Identity\n\n"
|
||||||
|
f"You are {profile.get('name', 'the Queen')}, {profile.get('title', 'Senior Advisor')}.\n\n"
|
||||||
|
f"{profile.get('summary', '')}"
|
||||||
|
]
|
||||||
|
|
||||||
|
experience = profile.get("experience")
|
||||||
|
if experience:
|
||||||
|
lines = ["\n\n## Experience"]
|
||||||
|
for entry in experience:
|
||||||
|
role = entry.get("role", "")
|
||||||
|
details = entry.get("details", [])
|
||||||
|
lines.append(f"\n- **{role}**")
|
||||||
|
for detail in details:
|
||||||
|
lines.append(f" - {detail}")
|
||||||
|
parts.append("\n".join(lines))
|
||||||
|
|
||||||
|
skills = profile.get("skills")
|
||||||
|
if skills:
|
||||||
|
parts.append(f"\n\n## Core Skills\n\n{skills}")
|
||||||
|
|
||||||
|
achievement = profile.get("signature_achievement")
|
||||||
|
if achievement:
|
||||||
|
parts.append(f"\n\n## Signature Achievement\n\n{achievement}")
|
||||||
|
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Queen selection (lightweight LLM classifier)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_QUEEN_SELECTOR_SYSTEM_PROMPT = """\
|
||||||
|
You are a routing classifier. Given a user's request, select the single best-matching \
|
||||||
|
queen identity from the list below.
|
||||||
|
|
||||||
|
Queens:
|
||||||
|
- queen_technology: Technical architecture, software engineering, infrastructure, DevOps, system design
|
||||||
|
- queen_growth: User acquisition, retention, growth experiments, PLG, marketing funnels, analytics
|
||||||
|
- queen_product_strategy: Product vision, roadmapping, user research, feature prioritization, go-to-market
|
||||||
|
- queen_finance_fundraising: Financial modeling, fundraising, investor relations, cap tables, unit economics, budgeting
|
||||||
|
- queen_legal: Contracts, IP, compliance, corporate governance, employment law, regulatory matters
|
||||||
|
- queen_brand_design: Brand identity, visual design, UX, design systems, creative direction, messaging
|
||||||
|
- queen_talent: Hiring, recruiting, team building, culture, compensation, organizational design
|
||||||
|
- queen_operations: Process optimization, vendor management, cross-functional coordination, project management
|
||||||
|
|
||||||
|
Reply with ONLY a valid JSON object — no markdown, no prose:
|
||||||
|
{"queen_id": "<one of the IDs above>"}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Pick the queen whose domain most directly applies to the user's request.
|
||||||
|
- If the request is about building software, coding, or technical systems, pick queen_technology.
|
||||||
|
- If the request spans multiple domains, pick the one most central to the ask.
|
||||||
|
- If truly ambiguous, default to queen_technology.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_DEFAULT_QUEEN_ID = "queen_technology"
|
||||||
|
|
||||||
|
|
||||||
|
async def select_queen(user_message: str, llm: LLMProvider) -> str:
|
||||||
|
"""Classify a user message into the best-matching queen ID.
|
||||||
|
|
||||||
|
Makes a single non-streaming LLM call. Returns the queen_id string.
|
||||||
|
Falls back to head-of-technology on any failure.
|
||||||
|
"""
|
||||||
|
if not user_message.strip():
|
||||||
|
return _DEFAULT_QUEEN_ID
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await llm.acomplete(
|
||||||
|
messages=[{"role": "user", "content": user_message}],
|
||||||
|
system=_QUEEN_SELECTOR_SYSTEM_PROMPT,
|
||||||
|
max_tokens=64,
|
||||||
|
json_mode=True,
|
||||||
|
)
|
||||||
|
raw = response.content.strip()
|
||||||
|
parsed = json.loads(raw)
|
||||||
|
queen_id = parsed.get("queen_id", "").strip()
|
||||||
|
if queen_id not in DEFAULT_QUEENS:
|
||||||
|
logger.warning("Queen selector returned unknown ID %r, falling back", queen_id)
|
||||||
|
return _DEFAULT_QUEEN_ID
|
||||||
|
logger.info("Queen selector: selected %s for request", queen_id)
|
||||||
|
return queen_id
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Queen selection failed, falling back to %s", _DEFAULT_QUEEN_ID, exc_info=True)
|
||||||
|
return _DEFAULT_QUEEN_ID
|
||||||
@@ -148,8 +148,8 @@ class EventType(StrEnum):
|
|||||||
# Queen phase changes (building <-> staging <-> running)
|
# Queen phase changes (building <-> staging <-> running)
|
||||||
QUEEN_PHASE_CHANGED = "queen_phase_changed"
|
QUEEN_PHASE_CHANGED = "queen_phase_changed"
|
||||||
|
|
||||||
# Queen thinking hook — persona selected for the current building session
|
# Queen identity — which queen profile was selected for this session
|
||||||
QUEEN_PERSONA_SELECTED = "queen_persona_selected"
|
QUEEN_IDENTITY_SELECTED = "queen_identity_selected"
|
||||||
|
|
||||||
# Subagent reports (one-way progress updates from sub-agents)
|
# Subagent reports (one-way progress updates from sub-agents)
|
||||||
SUBAGENT_REPORT = "subagent_report"
|
SUBAGENT_REPORT = "subagent_report"
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ def create_app(model: str | None = None) -> web.Application:
|
|||||||
from framework.server.routes_execution import register_routes as register_execution_routes
|
from framework.server.routes_execution import register_routes as register_execution_routes
|
||||||
from framework.server.routes_graphs import register_routes as register_graph_routes
|
from framework.server.routes_graphs import register_routes as register_graph_routes
|
||||||
from framework.server.routes_logs import register_routes as register_log_routes
|
from framework.server.routes_logs import register_routes as register_log_routes
|
||||||
|
from framework.server.routes_queens import register_routes as register_queen_routes
|
||||||
from framework.server.routes_sessions import register_routes as register_session_routes
|
from framework.server.routes_sessions import register_routes as register_session_routes
|
||||||
|
|
||||||
register_credential_routes(app)
|
register_credential_routes(app)
|
||||||
@@ -267,6 +268,7 @@ def create_app(model: str | None = None) -> web.Application:
|
|||||||
register_session_routes(app)
|
register_session_routes(app)
|
||||||
register_graph_routes(app)
|
register_graph_routes(app)
|
||||||
register_log_routes(app)
|
register_log_routes(app)
|
||||||
|
register_queen_routes(app)
|
||||||
|
|
||||||
# Static file serving — Option C production mode
|
# Static file serving — Option C production mode
|
||||||
# If frontend/dist/ exists, serve built frontend files on /
|
# If frontend/dist/ exists, serve built frontend files on /
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ async def create_queen(
|
|||||||
_queen_tools_staging,
|
_queen_tools_staging,
|
||||||
_shared_building_knowledge,
|
_shared_building_knowledge,
|
||||||
)
|
)
|
||||||
from framework.agents.queen.nodes.thinking_hook import select_expert_persona
|
from framework.agents.queen.queen_profiles import (
|
||||||
|
ensure_default_queens,
|
||||||
|
format_queen_identity_prompt,
|
||||||
|
load_queen_profile,
|
||||||
|
select_queen,
|
||||||
|
)
|
||||||
from framework.agent_loop.agent_loop import HookContext, HookResult
|
from framework.agent_loop.agent_loop import HookContext, HookResult
|
||||||
from framework.loader.mcp_registry import MCPRegistry
|
from framework.loader.mcp_registry import MCPRegistry
|
||||||
from framework.loader.tool_registry import ToolRegistry
|
from framework.loader.tool_registry import ToolRegistry
|
||||||
@@ -271,7 +276,7 @@ async def create_queen(
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Queen skill loading failed (non-fatal)", exc_info=True)
|
logger.debug("Queen skill loading failed (non-fatal)", exc_info=True)
|
||||||
|
|
||||||
# ---- Persona hook ------------------------------------------------
|
# ---- Queen identity hook -----------------------------------------
|
||||||
_session_llm = session.llm
|
_session_llm = session.llm
|
||||||
_session_event_bus = session.event_bus
|
_session_event_bus = session.event_bus
|
||||||
|
|
||||||
@@ -299,20 +304,32 @@ async def create_queen(
|
|||||||
filter_stream="queen",
|
filter_stream="queen",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _persona_hook(ctx: HookContext) -> HookResult | None:
|
async def _queen_identity_hook(ctx: HookContext) -> HookResult | None:
|
||||||
|
ensure_default_queens()
|
||||||
trigger = ctx.trigger or ""
|
trigger = ctx.trigger or ""
|
||||||
result = await select_expert_persona(trigger, _session_llm, memory_context="")
|
queen_id = await select_queen(trigger, _session_llm)
|
||||||
if not result:
|
try:
|
||||||
|
profile = load_queen_profile(queen_id)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning("Queen profile %s not found after selection", queen_id)
|
||||||
return None
|
return None
|
||||||
# Store on phase_state so persona/style persist across dynamic prompt refreshes
|
identity_prompt = format_queen_identity_prompt(profile)
|
||||||
phase_state.persona_prefix = result.persona_prefix
|
# Store on phase_state so identity persists across dynamic prompt refreshes
|
||||||
phase_state.style_directive = result.style_directive
|
phase_state.queen_id = queen_id
|
||||||
|
phase_state.queen_profile = profile
|
||||||
|
phase_state.queen_identity_prompt = identity_prompt
|
||||||
|
# Route session storage to ~/.hive/agents/queens/{queen_id}/sessions/
|
||||||
|
session.queen_name = queen_id
|
||||||
if _session_event_bus is not None:
|
if _session_event_bus is not None:
|
||||||
await _session_event_bus.publish(
|
await _session_event_bus.publish(
|
||||||
AgentEvent(
|
AgentEvent(
|
||||||
type=EventType.QUEEN_PERSONA_SELECTED,
|
type=EventType.QUEEN_IDENTITY_SELECTED,
|
||||||
stream_id="queen",
|
stream_id="queen",
|
||||||
data={"persona": result.persona_prefix},
|
data={
|
||||||
|
"queen_id": queen_id,
|
||||||
|
"name": profile.get("name", ""),
|
||||||
|
"title": profile.get("title", ""),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -351,7 +368,7 @@ async def create_queen(
|
|||||||
adjusted_node = _orig_node.model_copy(update=node_updates)
|
adjusted_node = _orig_node.model_copy(update=node_updates)
|
||||||
_queen_loop_config = {
|
_queen_loop_config = {
|
||||||
**_base_loop_config,
|
**_base_loop_config,
|
||||||
"hooks": {"session_start": [_persona_hook]},
|
"hooks": {"session_start": [_queen_identity_hook]},
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- Queen event loop (AgentLoop directly, no Orchestrator) -------
|
# ---- Queen event loop (AgentLoop directly, no Orchestrator) -------
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Queen identity profile routes.
|
||||||
|
|
||||||
|
- GET /api/queen/profiles — list all queen profiles (id, name, title)
|
||||||
|
- GET /api/queen/{queen_id}/profile — get full queen profile
|
||||||
|
- PATCH /api/queen/{queen_id}/profile — update queen profile fields
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from framework.agents.queen.queen_profiles import (
|
||||||
|
ensure_default_queens,
|
||||||
|
list_queens,
|
||||||
|
load_queen_profile,
|
||||||
|
update_queen_profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_list_profiles(request: web.Request) -> web.Response:
|
||||||
|
"""GET /api/queen/profiles — list all queen profiles."""
|
||||||
|
ensure_default_queens()
|
||||||
|
queens = list_queens()
|
||||||
|
return web.json_response({"queens": queens})
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_get_profile(request: web.Request) -> web.Response:
|
||||||
|
"""GET /api/queen/{queen_id}/profile — get full queen profile."""
|
||||||
|
queen_id = request.match_info["queen_id"]
|
||||||
|
ensure_default_queens()
|
||||||
|
try:
|
||||||
|
profile = load_queen_profile(queen_id)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return web.json_response({"error": f"Queen '{queen_id}' not found"}, status=404)
|
||||||
|
return web.json_response({"id": queen_id, **profile})
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_update_profile(request: web.Request) -> web.Response:
|
||||||
|
"""PATCH /api/queen/{queen_id}/profile — update queen profile fields."""
|
||||||
|
queen_id = request.match_info["queen_id"]
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return web.json_response({"error": "Invalid JSON body"}, status=400)
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return web.json_response({"error": "Body must be a JSON object"}, status=400)
|
||||||
|
try:
|
||||||
|
updated = update_queen_profile(queen_id, body)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return web.json_response({"error": f"Queen '{queen_id}' not found"}, status=404)
|
||||||
|
return web.json_response({"id": queen_id, **updated})
|
||||||
|
|
||||||
|
|
||||||
|
def register_routes(app: web.Application) -> None:
|
||||||
|
"""Register queen profile routes."""
|
||||||
|
app.router.add_get("/api/queen/profiles", handle_list_profiles)
|
||||||
|
app.router.add_get("/api/queen/{queen_id}/profile", handle_get_profile)
|
||||||
|
app.router.add_patch("/api/queen/{queen_id}/profile", handle_update_profile)
|
||||||
@@ -63,6 +63,8 @@ def _session_to_live_dict(session) -> dict:
|
|||||||
if phase_state
|
if phase_state
|
||||||
else ("staging" if session.graph_runtime else "planning"),
|
else ("staging" if session.graph_runtime else "planning"),
|
||||||
"queen_supports_images": supports_image_tool_results(queen_model) if queen_model else True,
|
"queen_supports_images": supports_image_tool_results(queen_model) if queen_model else True,
|
||||||
|
"queen_id": getattr(phase_state, "queen_id", None) if phase_state else None,
|
||||||
|
"queen_name": (phase_state.queen_profile or {}).get("name") if phase_state else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -583,9 +585,9 @@ async def handle_session_events_history(request: web.Request) -> web.Response:
|
|||||||
"""
|
"""
|
||||||
session_id = request.match_info["session_id"]
|
session_id = request.match_info["session_id"]
|
||||||
|
|
||||||
from framework.server.session_manager import _queen_session_dir
|
from framework.server.session_manager import _find_queen_session_dir
|
||||||
|
|
||||||
queen_dir = _queen_session_dir(session_id)
|
queen_dir = _find_queen_session_dir(session_id)
|
||||||
events_path = queen_dir / "events.jsonl"
|
events_path = queen_dir / "events.jsonl"
|
||||||
if not events_path.exists():
|
if not events_path.exists():
|
||||||
return web.json_response({"events": [], "session_id": session_id})
|
return web.json_response({"events": [], "session_id": session_id})
|
||||||
@@ -647,9 +649,9 @@ async def handle_delete_history_session(request: web.Request) -> web.Response:
|
|||||||
await manager.stop_session(session_id)
|
await manager.stop_session(session_id)
|
||||||
|
|
||||||
# Delete the queen session directory from disk
|
# Delete the queen session directory from disk
|
||||||
from framework.server.session_manager import _queen_session_dir
|
from framework.server.session_manager import _find_queen_session_dir
|
||||||
|
|
||||||
queen_session_dir = _queen_session_dir(session_id)
|
queen_session_dir = _find_queen_session_dir(session_id)
|
||||||
if queen_session_dir.exists() and queen_session_dir.is_dir():
|
if queen_session_dir.exists() and queen_session_dir.is_dir():
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(queen_session_dir)
|
shutil.rmtree(queen_session_dir)
|
||||||
@@ -701,9 +703,12 @@ async def handle_reveal_session_folder(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
session = manager.get_session(session_id)
|
session = manager.get_session(session_id)
|
||||||
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
|
||||||
from framework.server.session_manager import _queen_session_dir
|
if session:
|
||||||
|
from framework.server.session_manager import _queen_session_dir
|
||||||
folder = _queen_session_dir(storage_session_id)
|
folder = _queen_session_dir(storage_session_id, session.queen_name)
|
||||||
|
else:
|
||||||
|
from framework.server.session_manager import _find_queen_session_dir
|
||||||
|
folder = _find_queen_session_dir(storage_session_id)
|
||||||
folder.mkdir(parents=True, exist_ok=True)
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ def _queen_session_dir(session_id: str, queen_name: str = "default") -> Path:
|
|||||||
return QUEENS_DIR / queen_name / "sessions" / session_id
|
return QUEENS_DIR / queen_name / "sessions" / session_id
|
||||||
|
|
||||||
|
|
||||||
|
def _find_queen_session_dir(session_id: str) -> Path:
|
||||||
|
"""Search all queen directories for a session. Falls back to default."""
|
||||||
|
if QUEENS_DIR.exists():
|
||||||
|
for queen_dir in QUEENS_DIR.iterdir():
|
||||||
|
if not queen_dir.is_dir():
|
||||||
|
continue
|
||||||
|
candidate = queen_dir / "sessions" / session_id
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return _queen_session_dir(session_id)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Session:
|
class Session:
|
||||||
"""A live session with a queen and optional worker."""
|
"""A live session with a queen and optional worker."""
|
||||||
@@ -210,13 +222,15 @@ class SessionManager:
|
|||||||
# When cold-restoring, check meta.json for the phase — if the agent
|
# When cold-restoring, check meta.json for the phase — if the agent
|
||||||
# was still being built we must NOT try to load the worker (the code
|
# was still being built we must NOT try to load the worker (the code
|
||||||
# is incomplete and will fail to import).
|
# is incomplete and will fail to import).
|
||||||
|
_resume_queen_id: str | None = None
|
||||||
if queen_resume_from:
|
if queen_resume_from:
|
||||||
_resume_phase = None
|
_resume_phase = None
|
||||||
_meta_path = _queen_session_dir(queen_resume_from) / "meta.json"
|
_meta_path = _find_queen_session_dir(queen_resume_from) / "meta.json"
|
||||||
if _meta_path.exists():
|
if _meta_path.exists():
|
||||||
try:
|
try:
|
||||||
_meta = json.loads(_meta_path.read_text(encoding="utf-8"))
|
_meta = json.loads(_meta_path.read_text(encoding="utf-8"))
|
||||||
_resume_phase = _meta.get("phase")
|
_resume_phase = _meta.get("phase")
|
||||||
|
_resume_queen_id = _meta.get("queen_id")
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
pass
|
pass
|
||||||
if _resume_phase in ("building", "planning"):
|
if _resume_phase in ("building", "planning"):
|
||||||
@@ -237,6 +251,8 @@ class SessionManager:
|
|||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
session.queen_resume_from = queen_resume_from
|
session.queen_resume_from = queen_resume_from
|
||||||
|
if _resume_queen_id:
|
||||||
|
session.queen_name = _resume_queen_id
|
||||||
try:
|
try:
|
||||||
# Load the graph FIRST (before queen) so queen gets full tools
|
# Load the graph FIRST (before queen) so queen gets full tools
|
||||||
await self._load_worker_core(
|
await self._load_worker_core(
|
||||||
@@ -802,7 +818,10 @@ class SessionManager:
|
|||||||
_existing_meta = json.loads(_meta_path.read_text(encoding="utf-8"))
|
_existing_meta = json.loads(_meta_path.read_text(encoding="utf-8"))
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
pass
|
pass
|
||||||
_new_meta: dict = {"created_at": time.time()}
|
_new_meta: dict = {
|
||||||
|
"created_at": time.time(),
|
||||||
|
"queen_id": session.queen_name,
|
||||||
|
}
|
||||||
if _agent_name is not None:
|
if _agent_name is not None:
|
||||||
_new_meta["agent_name"] = _agent_name
|
_new_meta["agent_name"] = _agent_name
|
||||||
if session.worker_path is not None:
|
if session.worker_path is not None:
|
||||||
@@ -1094,7 +1113,7 @@ class SessionManager:
|
|||||||
~/.hive/agents/queens/{name}/sessions/{session_id}/conversations/. Returns None when
|
~/.hive/agents/queens/{name}/sessions/{session_id}/conversations/. Returns None when
|
||||||
no data is found so callers can fall through to a 404.
|
no data is found so callers can fall through to a 404.
|
||||||
"""
|
"""
|
||||||
queen_dir = _queen_session_dir(session_id)
|
queen_dir = _find_queen_session_dir(session_id)
|
||||||
convs_dir = queen_dir / "conversations"
|
convs_dir = queen_dir / "conversations"
|
||||||
if not convs_dir.exists():
|
if not convs_dir.exists():
|
||||||
return None
|
return None
|
||||||
@@ -1149,21 +1168,28 @@ class SessionManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def list_cold_sessions() -> list[dict]:
|
def list_cold_sessions() -> list[dict]:
|
||||||
"""Return metadata for every queen session directory on disk, newest first."""
|
"""Return metadata for every queen session directory on disk, newest first."""
|
||||||
queen_sessions_dir = QUEENS_DIR / "default" / "sessions"
|
if not QUEENS_DIR.exists():
|
||||||
if not queen_sessions_dir.exists():
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
results: list[dict] = []
|
# Collect session dirs from all queen identities
|
||||||
|
all_session_dirs: list[Path] = []
|
||||||
try:
|
try:
|
||||||
entries = sorted(
|
for queen_dir in QUEENS_DIR.iterdir():
|
||||||
queen_sessions_dir.iterdir(),
|
if not queen_dir.is_dir():
|
||||||
key=lambda p: p.stat().st_mtime,
|
continue
|
||||||
reverse=True,
|
sessions_dir = queen_dir / "sessions"
|
||||||
)
|
if sessions_dir.exists():
|
||||||
|
for d in sessions_dir.iterdir():
|
||||||
|
if d.is_dir():
|
||||||
|
all_session_dirs.append(d)
|
||||||
except OSError:
|
except OSError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
for d in entries:
|
# Sort all sessions by mtime, newest first
|
||||||
|
all_session_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
|
||||||
|
results: list[dict] = []
|
||||||
|
for d in all_session_dirs:
|
||||||
if not d.is_dir():
|
if not d.is_dir():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@@ -1238,6 +1264,9 @@ class SessionManager:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Derive queen_id from directory structure: queens/{queen_id}/sessions/{session_id}
|
||||||
|
queen_id = d.parent.parent.name if d.parent.name == "sessions" else None
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"session_id": d.name,
|
"session_id": d.name,
|
||||||
@@ -1249,6 +1278,7 @@ class SessionManager:
|
|||||||
"agent_path": agent_path,
|
"agent_path": agent_path,
|
||||||
"last_message": last_message,
|
"last_message": last_message,
|
||||||
"message_count": message_count,
|
"message_count": message_count,
|
||||||
|
"queen_id": queen_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ class QueenPhaseState:
|
|||||||
# Community skills catalog (XML) — appended after protocols
|
# Community skills catalog (XML) — appended after protocols
|
||||||
skills_catalog_prompt: str = ""
|
skills_catalog_prompt: str = ""
|
||||||
|
|
||||||
# Persona and communication style (set once at session start by persona hook,
|
# Queen identity (set once at session start by queen identity hook,
|
||||||
# persisted here so they survive dynamic prompt refreshes across iterations).
|
# persisted here so it survives dynamic prompt refreshes across iterations).
|
||||||
persona_prefix: str = "" # e.g. "You are a CFO. I am a CFO with 20 years..."
|
queen_id: str | None = None
|
||||||
style_directive: str = "" # e.g. "## Communication Style: Peer\n\n..."
|
queen_profile: dict | None = None
|
||||||
|
queen_identity_prompt: str = ""
|
||||||
|
|
||||||
# Cached global recall block — populated async by recall_selector after each turn.
|
# Cached global recall block — populated async by recall_selector after each turn.
|
||||||
_cached_global_recall_block: str = ""
|
_cached_global_recall_block: str = ""
|
||||||
@@ -164,11 +165,9 @@ class QueenPhaseState:
|
|||||||
base = self.prompt_building
|
base = self.prompt_building
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
if self.persona_prefix:
|
if self.queen_identity_prompt:
|
||||||
parts.append(self.persona_prefix)
|
parts.append(self.queen_identity_prompt)
|
||||||
parts.append(base)
|
parts.append(base)
|
||||||
if self.style_directive:
|
|
||||||
parts.append(self.style_directive)
|
|
||||||
if self.skills_catalog_prompt:
|
if self.skills_catalog_prompt:
|
||||||
parts.append(self.skills_catalog_prompt)
|
parts.append(self.skills_catalog_prompt)
|
||||||
if self.protocols_prompt:
|
if self.protocols_prompt:
|
||||||
|
|||||||
@@ -2078,6 +2078,17 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Initialize Queen Profiles
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
echo -n " ⬡ queen profiles... "
|
||||||
|
if uv run python -c "from framework.agents.queen.queen_profiles import ensure_default_queens; ensure_default_queens()" > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}ok${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}skipped${NC} ${DIM}(non-fatal)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Success!
|
# Success!
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user