refactor: simplify agent loading

This commit is contained in:
Timothy
2026-04-07 17:03:12 -07:00
parent db572b9be6
commit a5b17a293b
31 changed files with 508 additions and 755 deletions
+15 -63
View File
@@ -1,71 +1,23 @@
"""
Aden Hive Framework: A goal-driven agent runtime optimized for Builder observability.
"""Hive Agent Framework.
The runtime is designed around DECISIONS, not just actions. Every significant
choice the agent makes is captured with:
- What it was trying to do (intent)
- What options it considered
- What it chose and why
- What happened as a result
- Whether that was good or bad (evaluated post-hoc)
This gives the Builder LLM the information it needs to improve agent behavior.
## Testing Framework
The framework includes a Goal-Based Testing system (Goal Agent Eval):
- Generate tests from Goal success_criteria and constraints
- Mandatory user approval before tests are stored
- Parallel test execution with error categorization
- Debug tools with fix suggestions
See `framework.testing` for details.
Core classes:
AgentHost -- hosts agents, manages entry points and pipeline
Orchestrator -- routes between nodes in a graph
AgentLoop -- the LLM + tool execution loop (one per node)
AgentLoader -- loads agent.json from disk, builds pipeline
DecisionTracker -- records decisions for post-hoc analysis
"""
from framework.llm import LLMProvider
try:
from framework.llm import AnthropicProvider # noqa: F401
except ImportError:
pass
from framework.agent_loop import AgentLoop
from framework.host import AgentHost
from framework.loader import AgentLoader
from framework.tracker.decision_tracker import DecisionTracker # noqa: F401
from framework.schemas.decision import Decision, DecisionEvaluation, Option, Outcome
from framework.schemas.run import Problem, Run, RunSummary
# Testing framework
from framework.testing import (
ApprovalStatus,
DebugTool,
ErrorCategory,
Test,
TestResult,
TestStorage,
TestSuiteResult,
)
from framework.orchestrator import Orchestrator
from framework.tracker import DecisionTracker
__all__ = [
# Schemas
"Decision",
"Option",
"Outcome",
"DecisionEvaluation",
"Run",
"RunSummary",
"Problem",
# Runtime
"Runtime",
# LLM
"LLMProvider",
"AnthropicProvider",
# Runner
"AgentHost",
"AgentLoader",
# Testing
"Test",
"TestResult",
"TestSuiteResult",
"TestStorage",
"ApprovalStatus",
"ErrorCategory",
"DebugTool",
"AgentLoop",
"DecisionTracker",
"Orchestrator",
]
@@ -204,66 +204,6 @@ def build_escalate_tool() -> Tool:
},
)
def build_report_to_parent_tool() -> Tool:
"""Build the synthetic report_to_parent tool for sub-agent progress reports.
Sub-agents call this to send one-way progress updates, partial findings,
or status reports to the parent node (and external observers via event bus)
without blocking execution.
When ``wait_for_response`` is True, the sub-agent blocks until the parent
relays the user's response — used for escalation (e.g. login pages, CAPTCHAs).
When ``mark_complete`` is True, the sub-agent terminates immediately after
sending the report no need to call set_output for each output key.
"""
return Tool(
name="report_to_parent",
description=(
"Send a report to the parent agent. By default this is fire-and-forget: "
"the parent receives the report but does not respond. "
"Set wait_for_response=true to BLOCK until the user replies — use this "
"when you need human intervention (e.g. login pages, CAPTCHAs, "
"authentication walls). The user's response is returned as the tool result. "
"Set mark_complete=true to finish your task and terminate immediately "
"after sending the report — use this when your findings are in the "
"message/data fields and you don't need to call set_output."
),
parameters={
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "A human-readable status or progress message.",
},
"data": {
"type": "object",
"description": "Optional structured data to include with the report.",
},
"wait_for_response": {
"type": "boolean",
"description": (
"If true, block execution until the user responds. "
"Use for escalation scenarios requiring human intervention."
),
"default": False,
},
"mark_complete": {
"type": "boolean",
"description": (
"If true, terminate the sub-agent immediately after sending "
"this report. The report message and data are delivered to the "
"parent as the final result. No set_output calls are needed."
),
"default": False,
},
},
"required": ["message"],
},
)
def handle_set_output(
tool_input: dict[str, Any],
output_keys: list[str] | None,
@@ -28,7 +28,7 @@ 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, create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config
@@ -613,7 +613,7 @@ class CredentialTesterAgent:
graph = self._build_graph()
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=graph,
goal=goal,
storage_path=self._storage_path,
-1
View File
@@ -3,7 +3,6 @@
from framework.host.agent_host import ( # noqa: F401
AgentHost,
AgentRuntimeConfig,
create_agent_runtime,
)
from framework.host.event_bus import AgentEvent, EventBus, EventType # noqa: F401
from framework.host.execution_manager import ( # noqa: F401
+339 -445
View File
@@ -483,328 +483,7 @@ class AgentHost:
self._event_subscriptions.append(sub_id)
# Start timer-driven entry points
for ep_id, spec in self._entry_points.items():
if spec.trigger_type != "timer":
continue
tc = spec.trigger_config
cron_expr = tc.get("cron")
_raw_interval = tc.get("interval_minutes")
interval = float(_raw_interval) if _raw_interval is not None else None
run_immediately = tc.get("run_immediately", False)
if cron_expr:
# Cron expression mode — takes priority over interval_minutes
try:
from croniter import croniter
except ImportError as e:
raise RuntimeError(
"croniter is required for cron-based entry points. "
"Install it with: uv pip install croniter"
) from e
try:
if not croniter.is_valid(cron_expr):
raise ValueError(f"Invalid cron expression: {cron_expr}")
except ValueError as e:
logger.warning(
"Entry point '%s' has invalid cron config: %s",
ep_id,
e,
)
continue
def _make_cron_timer(
entry_point_id: str,
expr: str,
immediate: bool,
idle_timeout: float = 300,
):
async def _cron_loop():
from croniter import croniter
_persistent_session_id: str | None = None
if not immediate:
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
while self._running:
# Calculate next fire time upfront (used by skip paths too)
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
# Gate: skip tick if timers are explicitly paused
if self._timers_paused:
logger.debug(
"Cron '%s': paused, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
# Gate: skip tick if ANY stream is actively working.
# If the execution is idle (no LLM/tool activity
# beyond idle_timeout) let the timer proceed —
# execute() will cancel the stale execution.
_any_active = False
_min_idle = float("inf")
for _s in self._streams.values():
if _s.active_execution_ids:
_any_active = True
_idle = _s.agent_idle_seconds
if _idle < _min_idle:
_min_idle = _idle
logger.info(
"Cron '%s': gate — active=%s, idle=%.1fs, timeout=%ds",
entry_point_id,
_any_active,
_min_idle,
idle_timeout,
)
if _any_active and _min_idle < idle_timeout:
logger.info(
"Cron '%s': agent actively working, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
self._timer_next_fire.pop(entry_point_id, None)
try:
ep_spec = self._entry_points.get(entry_point_id)
is_isolated = ep_spec and ep_spec.isolation_level == "isolated"
if is_isolated:
if _persistent_session_id:
session_state = {
"resume_session_id": _persistent_session_id
}
else:
session_state = None
else:
session_state = self._get_primary_session_state(
exclude_entry_point=entry_point_id
)
# Gate: skip tick if no active session
if session_state is None:
logger.debug(
"Cron '%s': no active session, skipping",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
exec_id = await self.trigger(
entry_point_id,
{
"event": {
"source": "timer",
"reason": "scheduled",
}
},
session_state=session_state,
)
if not _persistent_session_id and is_isolated:
_persistent_session_id = exec_id
logger.info(
"Cron fired for entry point '%s' (expr: %s)",
entry_point_id,
expr,
)
except Exception:
logger.error(
"Cron trigger failed for '%s'",
entry_point_id,
exc_info=True,
)
# Calculate next fire from now
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
return _cron_loop
task = asyncio.create_task(
_make_cron_timer(
ep_id,
cron_expr,
run_immediately,
idle_timeout=float(tc.get("idle_timeout_seconds", 300)),
)()
)
self._timer_tasks.append(task)
logger.info(
"Started cron timer for entry point '%s' with expression '%s'%s",
ep_id,
cron_expr,
" (immediate first run)" if run_immediately else "",
)
elif interval and interval > 0:
# Fixed interval mode (original behavior)
def _make_timer(
entry_point_id: str,
mins: float,
immediate: bool,
idle_timeout: float = 300,
):
async def _timer_loop():
interval_secs = mins * 60
_persistent_session_id: str | None = None
if not immediate:
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
while self._running:
# Gate: skip tick if timers are explicitly paused
if self._timers_paused:
logger.debug(
"Timer '%s': paused, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
# Gate: skip tick if agent is actively working.
# Gate: skip tick if ANY stream is actively working.
_any_active = False
_min_idle = float("inf")
for _s in self._streams.values():
if _s.active_execution_ids:
_any_active = True
_idle = _s.agent_idle_seconds
if _idle < _min_idle:
_min_idle = _idle
logger.info(
"Timer '%s': gate — active=%s, idle=%.1fs, timeout=%ds",
entry_point_id,
_any_active,
_min_idle,
idle_timeout,
)
if _any_active and _min_idle < idle_timeout:
logger.info(
"Timer '%s': agent actively working, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
self._timer_next_fire.pop(entry_point_id, None)
try:
ep_spec = self._entry_points.get(entry_point_id)
is_isolated = ep_spec and ep_spec.isolation_level == "isolated"
if is_isolated:
if _persistent_session_id:
session_state = {
"resume_session_id": _persistent_session_id
}
else:
session_state = None
else:
session_state = self._get_primary_session_state(
exclude_entry_point=entry_point_id
)
# Gate: skip tick if no active session
if session_state is None:
logger.debug(
"Timer '%s': no active session, skipping",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
exec_id = await self.trigger(
entry_point_id,
{
"event": {
"source": "timer",
"reason": "scheduled",
}
},
session_state=session_state,
)
if not _persistent_session_id and is_isolated:
_persistent_session_id = exec_id
logger.info(
"Timer fired for entry point '%s' (next in %s min)",
entry_point_id,
mins,
)
except Exception:
logger.error(
"Timer trigger failed for '%s'",
entry_point_id,
exc_info=True,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
return _timer_loop
task = asyncio.create_task(
_make_timer(
ep_id,
interval,
run_immediately,
idle_timeout=float(tc.get("idle_timeout_seconds", 300)),
)()
)
self._timer_tasks.append(task)
logger.info(
"Started timer for entry point '%s' every %s min%s",
ep_id,
interval,
" (immediate first run)" if run_immediately else "",
)
else:
logger.warning(
"Entry point '%s' has trigger_type='timer' "
"but no 'cron' or valid 'interval_minutes' in trigger_config",
ep_id,
)
# Register primary graph
self._graphs[self._graph_id] = _GraphRegistration(
graph=self.graph,
goal=self.goal,
entry_points=dict(self._entry_points),
streams=dict(self._streams),
storage_subpath="",
event_subscriptions=list(self._event_subscriptions),
timer_tasks=list(self._timer_tasks),
timer_next_fire=self._timer_next_fire,
)
await self._start_timers()
# Start skill hot-reload watcher (no-op if watchfiles not installed)
await self._skills_manager.start_watching()
@@ -818,6 +497,332 @@ class AgentHost:
n_stages,
)
async def _start_timers(self) -> None:
"""Start timer-driven entry points (extracted from start())."""
for ep_id, spec in self._entry_points.items():
if spec.trigger_type != "timer":
continue
tc = spec.trigger_config
cron_expr = tc.get("cron")
_raw_interval = tc.get("interval_minutes")
interval = float(_raw_interval) if _raw_interval is not None else None
run_immediately = tc.get("run_immediately", False)
if cron_expr:
# Cron expression mode — takes priority over interval_minutes
try:
from croniter import croniter
except ImportError as e:
raise RuntimeError(
"croniter is required for cron-based entry points. "
"Install it with: uv pip install croniter"
) from e
try:
if not croniter.is_valid(cron_expr):
raise ValueError(f"Invalid cron expression: {cron_expr}")
except ValueError as e:
logger.warning(
"Entry point '%s' has invalid cron config: %s",
ep_id,
e,
)
continue
def _make_cron_timer(
entry_point_id: str,
expr: str,
immediate: bool,
idle_timeout: float = 300,
):
async def _cron_loop():
from croniter import croniter
_persistent_session_id: str | None = None
if not immediate:
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
while self._running:
# Calculate next fire time upfront (used by skip paths too)
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
# Gate: skip tick if timers are explicitly paused
if self._timers_paused:
logger.debug(
"Cron '%s': paused, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
# Gate: skip tick if ANY stream is actively working.
# If the execution is idle (no LLM/tool activity
# beyond idle_timeout) let the timer proceed —
# execute() will cancel the stale execution.
_any_active = False
_min_idle = float("inf")
for _s in self._streams.values():
if _s.active_execution_ids:
_any_active = True
_idle = _s.agent_idle_seconds
if _idle < _min_idle:
_min_idle = _idle
logger.info(
"Cron '%s': gate — active=%s, idle=%.1fs, timeout=%ds",
entry_point_id,
_any_active,
_min_idle,
idle_timeout,
)
if _any_active and _min_idle < idle_timeout:
logger.info(
"Cron '%s': agent actively working, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
self._timer_next_fire.pop(entry_point_id, None)
try:
ep_spec = self._entry_points.get(entry_point_id)
is_isolated = ep_spec and ep_spec.isolation_level == "isolated"
if is_isolated:
if _persistent_session_id:
session_state = {
"resume_session_id": _persistent_session_id
}
else:
session_state = None
else:
session_state = self._get_primary_session_state(
exclude_entry_point=entry_point_id
)
# Gate: skip tick if no active session
if session_state is None:
logger.debug(
"Cron '%s': no active session, skipping",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
continue
exec_id = await self.trigger(
entry_point_id,
{
"event": {
"source": "timer",
"reason": "scheduled",
}
},
session_state=session_state,
)
if not _persistent_session_id and is_isolated:
_persistent_session_id = exec_id
logger.info(
"Cron fired for entry point '%s' (expr: %s)",
entry_point_id,
expr,
)
except Exception:
logger.error(
"Cron trigger failed for '%s'",
entry_point_id,
exc_info=True,
)
# Calculate next fire from now
cron = croniter(expr, datetime.now())
next_dt = cron.get_next(datetime)
sleep_secs = (next_dt - datetime.now()).total_seconds()
self._timer_next_fire[entry_point_id] = (
time.monotonic() + sleep_secs
)
await asyncio.sleep(max(0, sleep_secs))
return _cron_loop
task = asyncio.create_task(
_make_cron_timer(
ep_id,
cron_expr,
run_immediately,
idle_timeout=float(tc.get("idle_timeout_seconds", 300)),
)()
)
self._timer_tasks.append(task)
logger.info(
"Started cron timer for entry point '%s' with expression '%s'%s",
ep_id,
cron_expr,
" (immediate first run)" if run_immediately else "",
)
elif interval and interval > 0:
# Fixed interval mode (original behavior)
def _make_timer(
entry_point_id: str,
mins: float,
immediate: bool,
idle_timeout: float = 300,
):
async def _timer_loop():
interval_secs = mins * 60
_persistent_session_id: str | None = None
if not immediate:
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
while self._running:
# Gate: skip tick if timers are explicitly paused
if self._timers_paused:
logger.debug(
"Timer '%s': paused, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
# Gate: skip tick if agent is actively working.
# Gate: skip tick if ANY stream is actively working.
_any_active = False
_min_idle = float("inf")
for _s in self._streams.values():
if _s.active_execution_ids:
_any_active = True
_idle = _s.agent_idle_seconds
if _idle < _min_idle:
_min_idle = _idle
logger.info(
"Timer '%s': gate — active=%s, idle=%.1fs, timeout=%ds",
entry_point_id,
_any_active,
_min_idle,
idle_timeout,
)
if _any_active and _min_idle < idle_timeout:
logger.info(
"Timer '%s': agent actively working, skipping tick",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
self._timer_next_fire.pop(entry_point_id, None)
try:
ep_spec = self._entry_points.get(entry_point_id)
is_isolated = ep_spec and ep_spec.isolation_level == "isolated"
if is_isolated:
if _persistent_session_id:
session_state = {
"resume_session_id": _persistent_session_id
}
else:
session_state = None
else:
session_state = self._get_primary_session_state(
exclude_entry_point=entry_point_id
)
# Gate: skip tick if no active session
if session_state is None:
logger.debug(
"Timer '%s': no active session, skipping",
entry_point_id,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
continue
exec_id = await self.trigger(
entry_point_id,
{
"event": {
"source": "timer",
"reason": "scheduled",
}
},
session_state=session_state,
)
if not _persistent_session_id and is_isolated:
_persistent_session_id = exec_id
logger.info(
"Timer fired for entry point '%s' (next in %s min)",
entry_point_id,
mins,
)
except Exception:
logger.error(
"Timer trigger failed for '%s'",
entry_point_id,
exc_info=True,
)
self._timer_next_fire[entry_point_id] = (
time.monotonic() + interval_secs
)
await asyncio.sleep(interval_secs)
return _timer_loop
task = asyncio.create_task(
_make_timer(
ep_id,
interval,
run_immediately,
idle_timeout=float(tc.get("idle_timeout_seconds", 300)),
)()
)
self._timer_tasks.append(task)
logger.info(
"Started timer for entry point '%s' every %s min%s",
ep_id,
interval,
" (immediate first run)" if run_immediately else "",
)
else:
logger.warning(
"Entry point '%s' has trigger_type='timer' "
"but no 'cron' or valid 'interval_minutes' in trigger_config",
ep_id,
)
# Register primary graph
self._graphs[self._graph_id] = _GraphRegistration(
graph=self.graph,
goal=self.goal,
entry_points=dict(self._entry_points),
streams=dict(self._streams),
storage_subpath="",
event_subscriptions=list(self._event_subscriptions),
timer_tasks=list(self._timer_tasks),
timer_next_fire=self._timer_next_fire,
)
async def stop(self) -> None:
"""Stop the agent runtime and all streams."""
if not self._running:
@@ -902,45 +907,27 @@ class AgentHost:
return self._streams.get(entry_point_id)
def _apply_pipeline_results(self) -> None:
"""Extract tools/LLM/credentials/skills from pipeline stages.
Called after ``pipeline.initialize_all()`` so stages have finished
their async setup (MCP connected, skills discovered, etc.).
The host reads stage properties and updates its own state.
"""
"""Read typed attributes from pipeline stages after initialization."""
for stage in self._pipeline.stages:
stage_name = stage.__class__.__name__
name = stage.__class__.__name__
# McpRegistryStage -> tools
if hasattr(stage, "tool_registry") and stage.tool_registry is not None:
if stage.tool_registry is not None:
tools = list(stage.tool_registry.get_tools().values())
executor = stage.tool_registry.get_executor()
if tools:
self._tools = tools
self._tool_executor = executor
logger.info(
"Pipeline injected %d tools from %s",
len(tools), stage_name,
)
self._tool_executor = stage.tool_registry.get_executor()
logger.info("Pipeline: %d tools from %s", len(tools), name)
# LlmProviderStage -> LLM
if hasattr(stage, "llm") and stage.llm is not None:
if self._llm is None:
self._llm = stage.llm
logger.info(
"Pipeline injected LLM from %s", stage_name,
)
if stage.llm is not None and self._llm is None:
self._llm = stage.llm
logger.info("Pipeline: LLM from %s", name)
# CredentialResolverStage -> accounts
if hasattr(stage, "accounts_prompt") and stage.accounts_prompt:
if stage.accounts_prompt:
self._accounts_prompt = stage.accounts_prompt
self._accounts_data = getattr(stage, "accounts_data", None)
self._tool_provider_map = getattr(
stage, "tool_provider_map", None,
)
self._accounts_data = stage.accounts_data
self._tool_provider_map = stage.tool_provider_map
# SkillRegistryStage -> skills manager
if hasattr(stage, "skills_manager") and stage.skills_manager is not None:
if stage.skills_manager is not None:
self._skills_manager = stage.skills_manager
@@ -1940,96 +1927,3 @@ class AgentHost:
# === CONVENIENCE FACTORY ===
def create_agent_runtime(
graph: "GraphSpec",
goal: "Goal",
storage_path: str | Path,
entry_points: list[EntryPointSpec],
llm: "LLMProvider | None" = None,
tools: list["Tool"] | None = None,
tool_executor: Callable | None = None,
config: AgentRuntimeConfig | None = None,
runtime_log_store: Any = None,
enable_logging: bool = True,
checkpoint_config: CheckpointConfig | None = None,
graph_id: str | None = None,
accounts_prompt: str = "",
accounts_data: list[dict] | None = None,
tool_provider_map: dict[str, str] | None = None,
event_bus: "EventBus | None" = None,
skills_manager_config: "SkillsManagerConfig | None" = None,
# Deprecated — pass skills_manager_config instead.
skills_catalog_prompt: str = "",
protocols_prompt: str = "",
skill_dirs: list[str] | None = None,
pipeline_stages: "list[PipelineStage] | None" = None,
) -> AgentHost:
"""
Create and configure an AgentHost with entry points.
Convenience factory that creates runtime and registers entry points.
Runtime logging is enabled by default for observability.
Args:
graph: Graph specification
goal: Goal driving execution
storage_path: Path for persistent storage
entry_points: Entry point specifications
llm: LLM provider
tools: Available tools
tool_executor: Tool executor function
config: Runtime configuration
runtime_log_store: Optional RuntimeLogStore for per-execution logging.
If None and enable_logging=True, creates one automatically.
enable_logging: Whether to enable runtime logging (default: True).
Set to False to disable logging entirely.
checkpoint_config: Optional checkpoint configuration for resumable sessions.
If None, uses default checkpointing behavior.
graph_id: Optional identifier for the primary graph (defaults to "primary").
accounts_data: Raw account data for per-node prompt generation.
tool_provider_map: Tool name to provider name mapping for account routing.
event_bus: Optional external EventBus to share with other components.
skills_catalog_prompt: Available skills catalog for system prompt.
protocols_prompt: Default skill operational protocols for system prompt.
skill_dirs: Skill base directories for Tier 3 resource access.
skills_manager_config: Skill configuration the runtime owns
discovery, loading, and prompt renderation internally.
skills_catalog_prompt: Deprecated. Pre-rendered skills catalog.
protocols_prompt: Deprecated. Pre-rendered operational protocols.
Returns:
Configured AgentRuntime (not yet started)
"""
# Auto-create runtime log store if logging is enabled and not provided
if enable_logging and runtime_log_store is None:
from framework.tracker.runtime_log_store import RuntimeLogStore
storage_path_obj = Path(storage_path) if isinstance(storage_path, str) else storage_path
runtime_log_store = RuntimeLogStore(storage_path_obj / "runtime_logs")
runtime = AgentHost(
graph=graph,
goal=goal,
storage_path=storage_path,
llm=llm,
tools=tools,
tool_executor=tool_executor,
config=config,
runtime_log_store=runtime_log_store,
checkpoint_config=checkpoint_config,
graph_id=graph_id,
accounts_prompt=accounts_prompt,
accounts_data=accounts_data,
tool_provider_map=tool_provider_map,
event_bus=event_bus,
skills_manager_config=skills_manager_config,
skills_catalog_prompt=skills_catalog_prompt,
protocols_prompt=protocols_prompt,
skill_dirs=skill_dirs,
pipeline_stages=pipeline_stages,
)
for spec in entry_points:
runtime.register_entry_point(spec)
return runtime
+23 -107
View File
@@ -25,9 +25,8 @@ from framework.orchestrator.node import NodeSpec
from framework.llm.provider import LLMProvider, Tool
from framework.loader.preload_validation import run_preload_validation
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost, AgentRuntimeConfig, create_agent_runtime
from framework.host.agent_host import AgentHost, AgentRuntimeConfig
from framework.host.execution_manager import EntryPointSpec
from framework.tracker.runtime_log_store import RuntimeLogStore
from framework.tools.flowchart_utils import generate_fallback_flowchart
logger = logging.getLogger(__name__)
@@ -1556,60 +1555,6 @@ class AgentLoader:
}
return self._tool_registry.register_mcp_server(server_config)
def _load_mcp_servers_from_config(self, config_path: Path) -> None:
"""Load and register MCP servers from a configuration file."""
self._tool_registry.load_mcp_config(config_path)
def _load_registry_mcp_servers(self, agent_path: Path) -> None:
"""Load and register MCP servers selected via ``mcp_registry.json``."""
registry_json = agent_path / "mcp_registry.json"
if registry_json.is_file():
self._tool_registry.set_mcp_registry_agent_path(agent_path)
else:
self._tool_registry.set_mcp_registry_agent_path(None)
from framework.loader.mcp_registry import MCPRegistry
try:
registry = MCPRegistry()
registry.initialize()
server_configs, selection_max_tools = registry.load_agent_selection(agent_path)
except Exception as exc:
logger.warning(
"Failed to load MCP registry servers for '%s': %s",
agent_path.name,
exc,
)
return
if not server_configs:
return
results = self._tool_registry.load_registry_servers(
server_configs,
preserve_existing_tools=True,
log_collisions=True,
max_tools=selection_max_tools,
)
loaded = [result for result in results if result["status"] == "loaded"]
skipped = [result for result in results if result["status"] != "loaded"]
logger.info(
"Loaded %d/%d MCP registry server(s) for agent '%s'",
len(loaded),
len(results),
agent_path.name,
)
if skipped:
logger.info(
"Skipped MCP registry servers for agent '%s': %s",
agent_path.name,
[
{"server": result["server"], "reason": result["skipped_reason"]}
for result in skipped
],
)
def set_approval_callback(self, callback: Callable) -> None:
"""
Set a callback for human-in-the-loop approval during execution.
@@ -1701,66 +1646,37 @@ class AgentLoader:
except Exception:
pass
self._setup_agent_runtime_with_pipeline(
pipeline_stages=pipeline_stages,
event_bus=event_bus,
)
def _setup_agent_runtime_with_pipeline(
self,
pipeline_stages: list,
event_bus=None,
) -> None:
"""Create AgentHost with pipeline stages."""
# Create AgentHost directly (no wrapper)
from framework.host.execution_manager import EntryPointSpec
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.tracker.runtime_log_store import RuntimeLogStore
entry_points = []
if self.graph.entry_node:
entry_points.append(
EntryPointSpec(
id="default",
name="Default",
entry_node=self.graph.entry_node,
trigger_type="manual",
isolation_level="shared",
),
)
log_store = RuntimeLogStore(
base_path=self._storage_path / "runtime_logs",
)
checkpoint_config = CheckpointConfig(
enabled=True,
checkpoint_on_node_start=False,
checkpoint_on_node_complete=True,
checkpoint_max_age_days=7,
async_checkpoint=True,
)
runtime_config = None
if self.runtime_config is not None:
from framework.host.agent_host import AgentRuntimeConfig
if isinstance(self.runtime_config, AgentRuntimeConfig):
runtime_config = self.runtime_config
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self.graph,
goal=self.goal,
storage_path=self._storage_path,
entry_points=entry_points,
llm=None, # Injected by LlmProviderStage
tools=[], # Injected by McpRegistryStage
tool_executor=None, # Injected by McpRegistryStage
runtime_log_store=log_store,
checkpoint_config=checkpoint_config,
config=runtime_config,
runtime_log_store=RuntimeLogStore(
base_path=self._storage_path / "runtime_logs",
),
checkpoint_config=CheckpointConfig(
enabled=True,
checkpoint_on_node_complete=True,
checkpoint_max_age_days=7,
async_checkpoint=True,
),
graph_id=self.graph.id or self.agent_path.name,
event_bus=event_bus,
pipeline_stages=pipeline_stages,
)
self._agent_runtime.register_entry_point(
EntryPointSpec(
id="default",
name="Default",
entry_node=self.graph.entry_node,
trigger_type="manual",
isolation_level="shared",
),
)
self._agent_runtime.intro_message = self.intro_message
def _get_api_key_env_var(self, model: str) -> str | None:
@@ -2190,7 +2106,7 @@ class AgentLoader:
except ImportError:
# aden_tools not installed - fall back to direct check
has_llm_nodes = any(
node.node_type in ("event_loop", "gcu") for node in self.graph.nodes
node.node_type == "event_loop" for node in self.graph.nodes
)
if has_llm_nodes:
api_key_env = self._get_api_key_env_var(self.model)
+1 -1
View File
@@ -607,7 +607,7 @@ class NodeWorker:
return self._node_impl
# Auto-create EventLoopNode
if self.node_spec.node_type in ("event_loop", "gcu"):
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.orchestrator.node import warn_if_deprecated_client_facing
+1 -2
View File
@@ -727,7 +727,6 @@ class Orchestrator:
VALID_NODE_TYPES = {
"event_loop",
"gcu",
}
# Node types removed in v0.5 — provide migration guidance
REMOVED_NODE_TYPES = {
@@ -773,7 +772,7 @@ class Orchestrator:
)
# Create based on type
if node_spec.node_type in ("event_loop", "gcu"):
if node_spec.node_type == "event_loop":
# Auto-create EventLoopNode with sensible defaults.
# Custom configs can still be pre-registered via node_registry.
from framework.agent_loop.agent_loop import AgentLoop, LoopConfig
+1 -4
View File
@@ -193,13 +193,10 @@ def build_system_prompt(spec: NodePromptSpec) -> str:
if spec.narrative:
parts.append(f"\n--- Context (what has happened so far) ---\n{spec.narrative}")
if not False and spec.node_type in ("event_loop", "gcu") and spec.output_keys:
if not False and spec.node_type == "event_loop" and spec.output_keys:
parts.append(f"\n{EXECUTION_SCOPE_PREAMBLE}")
if spec.node_type == "gcu":
from framework.orchestrator.gcu import GCU_BROWSER_SYSTEM_PROMPT
parts.append(f"\n{GCU_BROWSER_SYSTEM_PROMPT}")
if spec.focus_prompt:
parts.append(f"\n--- Current Focus ---\n{spec.focus_prompt}")
+24 -16
View File
@@ -8,7 +8,7 @@ from typing import Any, Literal
class PipelineRejectedError(Exception):
"""Raised by ``AgentRuntime.trigger`` when a stage rejects the request."""
"""Raised by ``AgentHost.trigger`` when a stage rejects the request."""
def __init__(self, stage_name: str, reason: str) -> None:
super().__init__(f"Pipeline rejected by {stage_name}: {reason}")
@@ -18,12 +18,7 @@ class PipelineRejectedError(Exception):
@dataclass
class PipelineContext:
"""Carries request data through the pipeline.
Stages can mutate ``metadata`` to pass information downstream -- e.g. a
cost-estimation stage might write ``metadata["estimated_cost"]`` so a
budget-guard stage later in the chain can reject requests over budget.
"""
"""Carries request data through the pipeline."""
entry_point_id: str
input_data: dict[str, Any]
@@ -44,25 +39,38 @@ class PipelineResult:
class PipelineStage(ABC):
"""Base class for all middleware stages.
Subclasses implement :meth:`process`. The class attribute ``order``
controls stage ordering (lower runs first; 100 is the default).
Stages may also implement :meth:`initialize` for one-time setup and
:meth:`post_process` to decorate the execution result.
Infrastructure stages (LLM, MCP, credentials, skills) set typed
attributes during ``initialize()`` that the host reads after all
stages have initialized. Request-level stages (rate limit, input
validation, cost guard) implement ``process()``.
Attributes set by infrastructure stages:
llm: LLM provider instance (set by LlmProviderStage)
tool_registry: ToolRegistry with discovered MCP tools (set by McpRegistryStage)
accounts_prompt: Connected accounts system prompt block (set by CredentialResolverStage)
accounts_data: Raw account info list (set by CredentialResolverStage)
tool_provider_map: Tool name -> provider mapping (set by CredentialResolverStage)
skills_manager: SkillsManager instance (set by SkillRegistryStage)
"""
order: int = 100
# Infrastructure stage outputs -- typed so _apply_pipeline_results
# doesn't need hasattr() sniffing.
llm: Any = None
tool_registry: Any = None
accounts_prompt: str = ""
accounts_data: list[dict] | None = None
tool_provider_map: dict[str, str] | None = None
skills_manager: Any = None
async def initialize(self) -> None:
"""Called once when the runtime starts."""
return None
@abstractmethod
async def process(self, ctx: PipelineContext) -> PipelineResult:
"""Process the incoming request.
Return a :class:`PipelineResult` with action ``continue``, ``reject``,
or ``transform``.
"""
"""Process the incoming request."""
async def post_process(self, ctx: PipelineContext, result: Any) -> Any:
"""Optional post-execution hook. Default: pass-through."""
+1 -2
View File
@@ -119,12 +119,11 @@ def classify_flowchart_node(
return FLOWCHART_REMAP[explicit]
node_id = node["id"]
node_type = node.get("node_type", "event_loop")
node_tools = set(node.get("tools") or [])
desc = (node.get("description") or "").lower()
# GCU / browser automation nodes → hexagon
if node_type == "gcu":
if False: # gcu removed
return "browser"
# Entry node (first node or no incoming edges) → start terminator
+1 -1
View File
@@ -68,7 +68,7 @@ def _node_to_dict(node: Any) -> dict:
tools_list = list(node.tools) if node.tools else []
if tools_list:
d["tools"] = {"policy": "explicit", "allowed": tools_list}
elif node.node_type == "gcu":
elif False: # gcu removed
d["tools"] = {"policy": "all"}
else:
d["tools"] = {"policy": "none"}
@@ -0,0 +1,10 @@
"""Queen lifecycle tools -- split into per-tool modules.
The main entry point is still ``register_queen_lifecycle_tools()`` in
``queen_lifecycle_tools.py``. This package provides the shared context
and individual tool registration functions.
"""
from framework.tools.queen_lifecycle.context import QueenToolContext
__all__ = ["QueenToolContext"]
@@ -0,0 +1,52 @@
"""Shared context for queen lifecycle tools.
All queen tools receive this context instead of closing over
individual variables from the registration function.
"""
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class QueenToolContext:
"""Shared state passed to all queen lifecycle tool implementations."""
session: Any # Session or WorkerSessionAdapter
session_manager: Any | None = None
manager_session_id: str | None = None
phase_state: Any | None = None # QueenPhaseState
registry: Any = None # ToolRegistry
def get_runtime(self):
"""Get current graph runtime from session (late-binding)."""
return getattr(self.session, "graph_runtime", None)
def update_meta(self, updates: dict) -> None:
"""Update session metadata JSON."""
if self.session_manager is None or self.manager_session_id is None:
return
try:
srv_session = self.session_manager.get_session(self.manager_session_id)
if srv_session is None:
return
meta_path = getattr(srv_session, "meta_path", None)
if meta_path is None:
return
import pathlib
meta_file = pathlib.Path(meta_path)
if meta_file.exists():
data = json.loads(meta_file.read_text(encoding="utf-8"))
else:
data = {}
data.update(updates)
meta_file.write_text(json.dumps(data, indent=2) + "\n")
except Exception:
logger.debug("Failed to update session meta", exc_info=True)
@@ -122,7 +122,7 @@ def tui(verbose: bool, debug: bool) -> None:
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -150,7 +150,7 @@ def tui(verbose: bool, debug: bool) -> None:
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
@@ -75,7 +75,7 @@ def tui(verbose, debug):
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -103,7 +103,7 @@ def tui(verbose, debug):
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
@@ -8,7 +8,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost, create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -244,7 +244,7 @@ class DeepResearchAgent:
)
]
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -84,7 +84,7 @@ def tui(mock, verbose, debug):
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -118,7 +118,7 @@ def tui(mock, verbose, debug):
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
@@ -8,7 +8,7 @@ from framework.orchestrator.edge import GraphSpec
from framework.orchestrator.orchestrator import ExecutionResult, Orchestrator
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -264,7 +264,7 @@ class EmailInboxManagementAgent:
),
]
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -52,7 +52,7 @@ def tui():
from framework.tui.app import AdenTUI
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
async def run_tui():
@@ -68,7 +68,7 @@ def tui():
api_key=agent.config.api_key,
api_base=agent.config.api_base,
)
runtime = create_agent_runtime(
runtime = AgentHost(
graph=agent._build_graph(),
goal=agent.goal,
storage_path=storage,
@@ -8,7 +8,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -159,7 +159,7 @@ class EmailReplyAgent:
tools = list(self._tool_registry.get_tools().values())
tool_executor = self._tool_registry.get_executor()
self._graph = self._build_graph()
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
+2 -2
View File
@@ -76,7 +76,7 @@ def tui(mock, verbose, debug):
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -106,7 +106,7 @@ def tui(mock, verbose, debug):
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
+2 -2
View File
@@ -8,7 +8,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost, create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config
@@ -224,7 +224,7 @@ class JobHunterAgent:
)
]
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -7,7 +7,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -125,7 +125,7 @@ class LocalBusinessExtractor:
tools = list(self._tool_registry.get_tools().values())
tool_executor = self._tool_registry.get_executor()
self._graph = self._build_graph()
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -55,7 +55,7 @@ def tui():
from framework.tui.app import AdenTUI
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
async def run_tui():
@@ -71,7 +71,7 @@ def tui():
api_key=agent.config.api_key,
api_base=agent.config.api_base,
)
runtime = create_agent_runtime(
runtime = AgentHost(
graph=agent._build_graph(),
goal=agent.goal,
storage_path=storage,
@@ -8,7 +8,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -165,7 +165,7 @@ class MeetingScheduler:
tools = list(self._tool_registry.get_tools().values())
tool_executor = self._tool_registry.get_executor()
self._graph = self._build_graph()
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
+2 -2
View File
@@ -8,7 +8,7 @@ from framework.orchestrator.edge import AsyncEntryPointSpec, GraphSpec
from framework.orchestrator.orchestrator import ExecutionResult
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import AgentHost, create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -265,7 +265,7 @@ class SDRAgent:
),
]
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -74,7 +74,7 @@ def tui(verbose, debug):
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -101,7 +101,7 @@ def tui(verbose, debug):
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
@@ -8,7 +8,7 @@ from framework.orchestrator.orchestrator import ExecutionResult
from framework.orchestrator.checkpoint_config import CheckpointConfig
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.execution_manager import EntryPointSpec
from .config import default_config, metadata
@@ -149,7 +149,7 @@ class TwitterNewsAgent:
tools = list(self._tool_registry.get_tools().values())
tool_executor = self._tool_registry.get_executor()
self._graph = self._build_graph()
self._agent_runtime = create_agent_runtime(
self._agent_runtime = AgentHost(
graph=self._graph,
goal=self.goal,
storage_path=self._storage_path,
@@ -77,7 +77,7 @@ def tui(mock, verbose, debug):
from framework.llm import LiteLLMProvider
from framework.loader.tool_registry import ToolRegistry
from framework.host.agent_host import create_agent_runtime
from framework.host.agent_host import AgentHost
from framework.host.event_bus import EventBus
from framework.host.execution_manager import EntryPointSpec
@@ -107,7 +107,7 @@ def tui(mock, verbose, debug):
tool_executor = agent._tool_registry.get_executor()
graph = agent._build_graph()
runtime = create_agent_runtime(
runtime = AgentHost(
graph=graph,
goal=agent.goal,
storage_path=storage_path,
+6 -19
View File
@@ -1478,13 +1478,11 @@ def run_agent_tests(
def validate_agent_package(agent_name: str) -> str:
"""Run structural validation checks on a built agent package in one call.
Executes 5 steps and reports all results (does not stop on first failure):
1. Class validation checks graph structure and entry_points contract
2. Node completeness every NodeSpec in nodes/ must be in the nodes list,
and GCU nodes must be referenced in a parent's sub_agents
3. Graph validation loads the agent graph without credential checks
4. Tool validation checks declared tools exist in MCP servers
5. Tests runs the agent's pytest suite
Executes validation steps and reports all results:
1. Schema validation loads agent.json via load_agent_config
2. Graph validation loads the agent graph via AgentLoader
3. Tool validation checks declared tools exist in MCP servers
4. Tests runs the agent's pytest suite (skipped if no tests/)
Note: Credential validation is intentionally skipped here (building phase).
Credentials are validated at run time by run_agent_with_input() preflight.
@@ -1522,19 +1520,8 @@ def validate_agent_package(agent_name: str) -> str:
pathlib.Path('exports/{agent_name}/agent.json').read_text()
)
g, goal = load_agent_config(data)
# Check GCU sub_agent references
sub_refs = set()
for n in g.nodes:
for sa in getattr(n, 'sub_agents', []) or []:
sub_refs.add(sa)
errors = []
for n in g.nodes:
if n.node_type == 'gcu' and n.id not in sub_refs:
errors.append(
f"GCU node '{{n.id}}' not in any node's sub_agents"
)
print(json.dumps({{
'valid': len(errors) == 0,
'valid': True,
'nodes': len(g.nodes),
'edges': len(g.edges),
'entry': g.entry_node,