Subtract dead agent husks and unify the gateway executor path.

Remove SpecialistAgentRunner, plan_agent_v2 re-export shims, empty runtime husks, and disconnected Admin knobs. Fold ops into build_gateway_executor, align AIA_ENABLE_PLUGIN_TOOLS with catalog, and allow ops on the default MCP specialist list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-11 01:02:29 +08:00
parent e2fc72607e
commit 628a9dffd2
32 changed files with 127 additions and 870 deletions

View file

@ -6,10 +6,9 @@ import os
from typing import Any
from runtime.agents.agent_scope import resolve_default_agent_id
from runtime.agents.network_ops_agent import NetworkOpsAgent
from runtime.agents.specialist_agent import SpecialistProfile
from runtime.agents.specialists import (
AGENT_PROFILE_BINDINGS_KEY,
SpecialistProfile,
normalize_specialist_id,
agent_role_ids,
MANAGER_AGENT_ID,
@ -63,7 +62,7 @@ def _build_executor_components(
viewer_username: str | None = None,
viewer_tenant_id: str | None = None,
) -> tuple[
NetworkOpsAgent,
Agent,
dict[str, SpecialistProfile],
object,
str,
@ -205,9 +204,15 @@ def _build_executor_components(
specialist_models[sid] = m
specialist_modes[sid] = md
base_agent = NetworkOpsAgent(
base_agent = Agent(
store=store,
tools=default_registry(
expert=expert_name_for_specialist("ops"),
specialist="ops",
store=store,
),
model=specialist_models.get("ops") or active_model,
system_prompt=default_system_prefix_for_specialist("ops", lang),
lang=lang,
llm_profile_mode=specialist_modes.get("ops") or active_mode,
)
@ -315,17 +320,6 @@ def build_gateway_executor(
prof = specialist_profiles.get(sid) or specialist_profiles["generalist"]
chosen_model = specialist_models.get(prof.name) or base_agent.model
chosen_mode = specialist_modes.get(prof.name) or getattr(base_agent, "llm_profile_mode", None)
if prof.name == "ops":
return NetworkOpsAgent(
store=store,
model=chosen_model,
lang=(lang or "zh").strip().lower(),
llm_profile_mode=chosen_mode,
system_prompt=prof.system_prefix,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
)
reg_kw: dict[str, Any] = {
"expert": expert_name_for_specialist(prof.name),
"specialist": prof.name,

View file

@ -1,9 +1,15 @@
from __future__ import annotations
"""Ops specialist helpers.
Prefer ``build_gateway_executor(specialist=\"ops\")``. This module keeps the
legacy ``NetworkOpsAgent`` name as a thin ``Agent`` factory for older imports.
"""
from typing import Any
from runtime.chat.agent import Agent
from runtime.agent_context import build_role_system_context
from runtime.chat.agent import Agent
from svc.persistence.sqlite_store import SqliteStore
from runtime.tools import default_registry
@ -12,7 +18,10 @@ NETWORK_SYSTEM_PROMPT_ZH = build_role_system_context("ops")
class NetworkOpsAgent(Agent):
"""网络运维专家 Agent:固定专家提示词与专家工具目录。"""
"""Compatibility alias: ops specialist with network_ops(+memory) tool catalog.
New code should use ``runtime.agents.factory.build_gateway_executor(specialist=\"ops\")``.
"""
def __init__(
self,
@ -27,7 +36,8 @@ class NetworkOpsAgent(Agent):
path_policy_user_id: str | None = None,
) -> None:
tools = default_registry(
expert="network_ops",
expert="network_ops+memory",
specialist="ops",
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,

View file

@ -1,478 +0,0 @@
from __future__ import annotations
import json
import os
import sys
import time
import hashlib
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, Optional
from runtime.chat.agent import Agent
from runtime.chat.agent import GenerationInterrupted
from runtime.agents.network_ops_agent import NetworkOpsAgent
from svc.persistence.sqlite_store import SqliteStore
from svc.llm.image_legacy_client import (
IMAGE_SPECIALIST_DEFAULT_PROMPT_ZH,
collect_legacy_lane_images_from_attachments,
collect_legacy_lane_images_with_session_fallback,
legacy_image_assistant_body_with_placeholder,
legacy_image_turn_bundle,
send_legacy_image_messages,
)
from svc.llm.video_generation_client import (
VIDEO_SPECIALIST_DEFAULT_PROMPT_ZH,
legacy_video_assistant_body_with_placeholder,
legacy_video_turn_bundle,
send_video_generation_request,
)
from runtime.tools import default_registry
from runtime.agents.specialists import expert_name_for_specialist
from runtime.chat.turn_types import TurnRunOutcome
from runtime.relay_pointer import build_manifest_from_attachment_refs
from runtime.types import RelayShareEnvelope
from runtime.orchestration.protocol import (
AgentTask,
PlanStep,
SpecialistDelivery,
SpecialistResult,
SpecialistToolTrace,
)
@dataclass(frozen=True)
class SpecialistProfile:
name: str
system_prefix: str
tool_tags: frozenset[str] | None = None
@dataclass
class SpecialistAgentRunner:
store: SqliteStore
model: Any
llm_profile_mode: str | None
lang: str
profiles: dict[str, SpecialistProfile] = field(default_factory=dict)
model_by_specialist: dict[str, Any] = field(default_factory=dict)
llm_mode_by_specialist: dict[str, str | None] = field(default_factory=dict)
_agent_cache: dict[tuple, Agent] = field(default_factory=dict, init=False, repr=False)
@staticmethod
def _allowlist_mutation_fingerprint(
store: SqliteStore,
*,
policy_session_id: str | None = None,
path_policy_tenant_id: str | None = None,
path_policy_user_id: str | None = None,
) -> str:
t = (path_policy_tenant_id or "").strip() or None
u = (path_policy_user_id or "").strip() or None
if (not t or not u) and (policy_session_id or "").strip():
try:
own = store.get_ui_session_owner(session_id=str(policy_session_id).strip()) or {}
except Exception:
own = {}
t = t or (str(own.get("tenant_id") or "").strip() or None)
u = u or (str(own.get("user_id") or "").strip() or None)
if not t or not u:
return "0"
try:
row = store.get_user_workspace_path_allowlist(tenant_id=t, user_id=u)
except Exception:
row = None
if not row or not isinstance(row, dict):
return "0|"
er = str(row.get("extra_roots") or "")
return f"{1 if int(row.get('allow_any_path') or 0) else 0}|{str(row.get('updated_at') or '')}|{er[:2000]}"
def _agent_cache_fingerprint(
self,
specialist: str,
prof: SpecialistProfile,
*,
policy_session_id: str | None = None,
path_policy_tenant_id: str | None = None,
path_policy_user_id: str | None = None,
) -> str:
tool_names: list[str] = []
try:
regs = default_registry(
expert=expert_name_for_specialist(prof.name),
specialist=prof.name,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
store=self.store,
)
tool_names = sorted([str(t.name) for t in regs.list()])
except Exception:
tool_names = []
raw = json.dumps(
{
"specialist": specialist,
"profile_name": prof.name,
"system_prefix": prof.system_prefix,
"tool_names": tool_names,
"tool_tags": sorted(list(prof.tool_tags or frozenset())),
"policy_session_tail": (str(policy_session_id or "")[-16:]),
"allowlist_fp": self._allowlist_mutation_fingerprint(
self.store,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
),
},
ensure_ascii=False,
sort_keys=True,
)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
def _resolve_profile_and_model(self, specialist: str) -> tuple[SpecialistProfile, Any, str | None]:
prof = self.profiles.get(specialist) or self.profiles["generalist"]
chosen_model = self.model_by_specialist.get(prof.name) or self.model
chosen_mode = self.llm_mode_by_specialist.get(prof.name) or self.llm_profile_mode
return prof, chosen_model, chosen_mode
def _build_agent_for(
self,
specialist: str,
*,
policy_session_id: str | None = None,
use_cache: bool = True,
path_policy_tenant_id: str | None = None,
path_policy_user_id: str | None = None,
) -> Agent:
prof, chosen_model, chosen_mode = self._resolve_profile_and_model(specialist)
cache_fp = self._agent_cache_fingerprint(
specialist,
prof,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
)
alfp = self._allowlist_mutation_fingerprint(
self.store,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
)
cache_key = (prof.name, id(chosen_model), chosen_mode, self.lang, cache_fp, str(policy_session_id or ""), alfp)
if use_cache:
cached = self._agent_cache.get(cache_key)
if cached is not None:
return cached
if prof.name == "ops":
agent: Agent = NetworkOpsAgent(
store=self.store,
model=chosen_model,
lang=self.lang,
llm_profile_mode=chosen_mode,
system_prompt=prof.system_prefix,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
)
if use_cache:
self._agent_cache[cache_key] = agent
return agent
tools = default_registry(
expert=expert_name_for_specialist(prof.name),
specialist=prof.name,
policy_session_id=policy_session_id,
path_policy_tenant_id=path_policy_tenant_id,
path_policy_user_id=path_policy_user_id,
store=self.store,
)
agent = Agent(
store=self.store,
tools=tools,
model=chosen_model,
system_prompt=prof.system_prefix,
lang=self.lang,
llm_profile_mode=chosen_mode,
)
if use_cache:
self._agent_cache[cache_key] = agent
return agent
def run_specialist(
self,
*,
parent_task: AgentTask,
step: PlanStep,
session_id: str | None = None,
use_cache: bool = True,
on_progress: Optional[Callable[[str], None]] = None,
on_token: Optional[Callable[[str], None]] = None,
on_tool_ui: Optional[Callable[[str, dict[str, Any]], None]] = None,
should_stop: Optional[Callable[[], bool]] = None,
) -> SpecialistResult:
started = time.perf_counter()
if on_progress:
obj = (step.objective or "").strip().replace("\n", " ")
if len(obj) > 140:
obj = obj[:137] + "..."
on_progress(f"[sp.start] {step.step_id} specialist={step.specialist} objective={obj}")
created_session_id: str | None = None
if not session_id:
temp_session = self.store.create_session(f"specialist:{step.specialist}")
session_id = temp_session.id
created_session_id = session_id
# User chat session for workspace/MCP path policy (specialist temp session usually has no ui_session_owner).
_raw_policy_sid = str(parent_task.session_id or "").strip() or str(session_id or "").strip()
policy_session_id: str | None = _raw_policy_sid if _raw_policy_sid else None
_meta: dict[str, Any] = parent_task.metadata if isinstance(getattr(parent_task, "metadata", None), dict) else {}
_path_tenant = str(_meta.get("tenant_id") or "").strip() or None
_path_user = str(_meta.get("user_id") or "").strip() or None
prompt = (
f"Specialist: {step.specialist}\n"
f"Objective: {step.objective}\n"
f"Parent user request: {parent_task.user_text}\n"
f"Step input: {step.input_text}\n"
"Execution policy: when the user asks to read/open/list/summarize concrete files, URLs, or MCP resources, "
"execute with available tools first. Do not return generic optimization plans unless explicitly requested.\n"
)
image_input_count = 0
image_input_kind: list[str] = []
image_protocol = ""
image_debug_schema = ""
image_debug_payload: dict[str, Any] | str = {}
specialist_delivery: SpecialistDelivery | None = None
try:
if step.specialist == "image":
image_protocol = "messages.content.image"
selected_images = collect_legacy_lane_images_from_attachments(
list(parent_task.attachments or []),
max_images=3,
)
image_input_count = len(selected_images)
image_input_kind = ["data_url" if s.startswith("data:") else "url" for s in selected_images]
if not selected_images:
output = "Image specialist received no image input."
ok = False
else:
# Use the user's chosen model/session profile (same as specialist routing UI). Wrong model ⇒ upstream HTTP error as-is (no OCR lane, no alternate payload).
_, chosen_model, _ = self._resolve_profile_and_model(step.specialist)
text_parts = [
str(x).strip()
for x in (step.objective, step.input_text, parent_task.user_text)
if str(x or "").strip()
]
user_text = "\n".join(text_parts) if text_parts else IMAGE_SPECIALIST_DEFAULT_PROMPT_ZH
if str(os.getenv("AIA_IMAGE_EXPERT_DEBUG_PRINT_PAYLOAD") or "").strip().lower() in (
"1",
"true",
"yes",
"on",
):
try:
sys.stderr.write(
"[oclaw specialist:image] lane=legacy_http → send_legacy_image_messages "
"(NOT OpenAIResponsesModel).\n"
)
sys.stderr.flush()
except Exception:
pass
resp = send_legacy_image_messages(
images=selected_images,
prompt=user_text,
model=str(getattr(chosen_model, "model", "") or "").strip() or None,
api_key=str(getattr(chosen_model, "api_key", "") or "").strip() or None,
base_url=str(getattr(chosen_model, "base_url", "") or "").strip() or None,
)
image_debug_schema = str(resp.get("debug_used_schema") or "").strip()
dbg = resp.get("debug_used_debug")
if isinstance(dbg, dict):
image_debug_payload = dbg
elif dbg is not None:
image_debug_payload = str(dbg)
ok, output, produced_attachments = legacy_image_turn_bundle(resp)
output = legacy_image_assistant_body_with_placeholder(
lang=self.lang,
body_text=output,
produced=produced_attachments if ok else None,
)
self.store.add_message(
session_id=session_id,
role="assistant",
content=output,
attachments=produced_attachments or None,
)
specialist_delivery = SpecialistDelivery(
specialist=step.specialist,
step_id=step.step_id,
answer_text=str(output or ""),
tool_traces=(),
notes="image_pipeline",
)
elif step.specialist == "video":
image_protocol = "video_generation.http"
_, chosen_model, _ = self._resolve_profile_and_model(step.specialist)
text_parts = [
str(x).strip()
for x in (step.objective, step.input_text, parent_task.user_text)
if str(x or "").strip()
]
user_text_v = "\n".join(text_parts) if text_parts else VIDEO_SPECIALIST_DEFAULT_PROMPT_ZH
policy_sid = str(policy_session_id or parent_task.session_id or session_id or "").strip()
v_frames, _v_src = collect_legacy_lane_images_with_session_fallback(
store=self.store,
session_id=policy_sid,
attachments=list(parent_task.attachments or []),
max_images=1,
)
v_frame = str(v_frames[0]).strip() if v_frames else None
resp_v = send_video_generation_request(
prompt=user_text_v,
model=str(getattr(chosen_model, "model", "") or "").strip() or None,
api_key=str(getattr(chosen_model, "api_key", "") or "").strip() or None,
base_url=str(getattr(chosen_model, "base_url", "") or "").strip() or None,
img_url=v_frame,
on_progress=on_progress,
should_stop=should_stop,
)
ok, output, produced_attachments = legacy_video_turn_bundle(resp_v)
output = legacy_video_assistant_body_with_placeholder(
lang=self.lang,
body_text=output,
produced=produced_attachments if ok else None,
)
self.store.add_message(
session_id=session_id,
role="assistant",
content=output,
attachments=(produced_attachments or None) if ok else None,
)
specialist_delivery = SpecialistDelivery(
specialist=step.specialist,
step_id=step.step_id,
answer_text=str(output or ""),
tool_traces=(),
notes="video_pipeline",
)
else:
agent = self._build_agent_for(
step.specialist,
policy_session_id=policy_session_id,
use_cache=use_cache,
path_policy_tenant_id=_path_tenant,
path_policy_user_id=_path_user,
)
from runtime.gateway import OclawGateway
from runtime.types import StandardMessage
gw = OclawGateway(store=self.store)
msg = StandardMessage(
session_id=str(session_id),
tenant_id=str(_path_tenant or ""),
user_id=str(_path_user or ""),
role="member",
channel="specialist",
text=str(prompt or ""),
attachments=list(parent_task.attachments or []),
metadata={
"tenant_id": str(_path_tenant or ""),
"user_id": str(_path_user or ""),
"channel": f"specialist:{step.specialist}",
},
)
output = gw.handle_turn(
msg=msg,
lang=str(getattr(agent, "lang", "zh") or "zh"),
executor=agent,
on_token=on_token,
on_progress=on_progress,
on_tool_ui=on_tool_ui,
should_stop=should_stop,
).reply_text
ok = bool((output or "").strip())
outcome = getattr(agent, "_last_turn_outcome", None)
if isinstance(outcome, TurnRunOutcome):
traces = tuple(
SpecialistToolTrace(
name=str(x.get("name") or ""),
ok=bool(x.get("ok")),
latency_ms=int(x.get("latency_ms") or x.get("duration_ms") or 0),
)
for x in outcome.tool_traces
)
specialist_delivery = SpecialistDelivery(
specialist=step.specialist,
step_id=step.step_id,
answer_text=str(output or ""),
tool_traces=traces,
notes=str(outcome.handoff_note or ""),
)
except GenerationInterrupted:
raise
except Exception as e:
output = f"{type(e).__name__}: {e}"
ok = False
finally:
produced_attachments: list[dict[str, Any]] = []
try:
rows = self.store.get_messages(session_id=session_id, limit=40) if session_id else []
for m in reversed(rows):
if str(m.role) != "assistant":
continue
if not m.attachments:
continue
raw = json.loads(m.attachments)
if isinstance(raw, list):
produced_attachments = [a for a in raw if isinstance(a, dict)]
break
except Exception:
produced_attachments = []
if created_session_id:
try:
parent_sid = str(parent_task.session_id or "").strip()
if parent_sid and parent_sid != str(created_session_id):
# Preserve tool usage telemetry: tool uses run inside temp specialist sessions.
# If we delete temp sessions directly, FK cascade would drop those tool_log rows.
self.store.move_tool_logs_to_session(
from_session_id=str(created_session_id),
to_session_id=parent_sid,
)
except Exception:
pass
self.store.delete_session(created_session_id)
latency = int((time.perf_counter() - started) * 1000)
if on_progress:
on_progress(
f"[sp.done] {step.step_id} specialist={step.specialist} ok={ok} latency_ms={latency}"
)
scope_id = str(session_id or parent_task.session_id or "").strip()
manifest = build_manifest_from_attachment_refs(
produced_attachments,
scope_id=scope_id,
source_agent=str(step.specialist or ""),
ttl_policy="turn",
)
relay_env = RelayShareEnvelope(
schema_version="v1",
trace_id=str((parent_task.metadata or {}).get("trace_id") or ""),
run_id=str((parent_task.metadata or {}).get("run_id") or ""),
attempt_no=int((parent_task.metadata or {}).get("attempt_no") or 0),
attachments=manifest,
)
return SpecialistResult(
step_id=step.step_id,
specialist=step.specialist,
success=ok,
output_text=output,
latency_ms=latency,
metadata={
"objective": step.objective,
"attachments": produced_attachments,
"relay_share_envelope": relay_env.to_dict(),
"image_input_count": image_input_count,
"image_input_kind": image_input_kind,
"image_protocol": image_protocol,
"image_debug_schema": image_debug_schema,
"image_debug_payload": image_debug_payload,
},
delivery=specialist_delivery,
)

View file

@ -21,6 +21,15 @@ class SpecialistConfig:
default_tool_tags: frozenset[str] | None
@dataclass(frozen=True)
class SpecialistProfile:
"""Prompt/tool surface for a specialist id (gateway executor factory)."""
name: str
system_prefix: str
tool_tags: frozenset[str] | None = None
SPECIALISTS: dict[SpecialistId, SpecialistConfig] = {
"ops": SpecialistConfig(
specialist_id="ops",
@ -140,6 +149,7 @@ __all__ = [
"MANAGER_AGENT_ID",
"SpecialistConfig",
"SpecialistId",
"SpecialistProfile",
"SPECIALISTS",
"specialist_ids",
"default_system_prefix_for_specialist",

View file

@ -1,2 +0,0 @@
"""Application-facing runtime entrypoints."""

View file

@ -1,34 +0,0 @@
from __future__ import annotations
import time
from typing import Any
from svc.persistence.sqlite_store import SqliteStore
class ToolAuditAdapter:
def __init__(self, store: SqliteStore):
self.store = store
def log_dispatch(
self,
*,
session_id: str,
specialist: str,
task_kind: str,
action: str,
payload: dict[str, Any],
status: str = "ok",
reason: str = "",
) -> None:
started = time.perf_counter()
self.store.add_agent_audit_log(
session_id=session_id,
specialist=specialist,
task_kind=task_kind,
action=action,
payload=payload,
status=status,
reason=reason,
duration_ms=max(0, int((time.perf_counter() - started) * 1000)),
)

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.adapter import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.compat import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.gateway_adapter import * # noqa: F403

View file

@ -1,86 +0,0 @@
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass
from typing import Any
from runtime.gateway import OclawGatewayResult
from runtime.plan_agent_v2 import (
build_shadow_gateway_result,
evaluate_gateway_expert_turn_shadow,
)
from runtime.types import StandardMessage
@dataclass(frozen=True)
class GatewayCutoverDraftOutput:
handled: bool
result: OclawGatewayResult | None
system_prompt_override: str = ""
decision_action: str = ""
def maybe_handle_expert_turn_v2_draft(
*,
store: Any,
msg: StandardMessage,
lang: str,
interaction_mode: str,
requested_specialist: str,
base_system_prompt: str,
force_flag: bool = False,
) -> GatewayCutoverDraftOutput:
"""Draft-only helper for future gateway cutover.
Important:
- This module is intentionally NOT wired into `runtime/gateway.py`.
- It documents and validates the minimal cutover behavior in isolation.
"""
t0 = time.perf_counter()
trace_id = str(uuid.uuid4())
run_id = str(uuid.uuid4())
shadow = evaluate_gateway_expert_turn_shadow(
store=store,
msg=msg,
lang=lang,
interaction_mode=interaction_mode,
requested_specialist=requested_specialist,
base_system_prompt=base_system_prompt,
force_flag=force_flag,
trace_id=trace_id,
parent_span_id=None,
)
if not shadow.used_v2 or shadow.decision is None:
return GatewayCutoverDraftOutput(handled=False, result=None)
action = str(shadow.decision.action or "")
elapsed_ms = int((time.perf_counter() - t0) * 1000)
if action in {"enter_plan", "stay_plan"}:
row = build_shadow_gateway_result(
decision=shadow.decision,
run_id=run_id,
trace_id=trace_id,
elapsed_ms=elapsed_ms,
requested_specialist=requested_specialist,
)
result = OclawGatewayResult(**row)
return GatewayCutoverDraftOutput(
handled=True,
result=result,
decision_action=action,
system_prompt_override="",
)
# run_agent: draft suggests continuing legacy execution with injected prompt.
return GatewayCutoverDraftOutput(
handled=False,
result=None,
decision_action=action,
system_prompt_override=str(shadow.decision.system_prompt_override or ""),
)
__all__ = ["GatewayCutoverDraftOutput", "maybe_handle_expert_turn_v2_draft"]

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.manager import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.models import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.prompt_injector import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.state_store import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.switch import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.tool_policy import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.tool_specs import * # noqa: F403

View file

@ -1,2 +0,0 @@
from runtime.plan_agent_v2.trace import * # noqa: F403

View file

@ -3,14 +3,12 @@
from __future__ import annotations
from runtime.tools.catalog import (
TOOL_FACTORIES,
default_registry,
materialize_tool_specs,
tool_inventory,
)
__all__ = [
"TOOL_FACTORIES",
"default_registry",
"materialize_tool_specs",
"tool_inventory",

View file

@ -25,15 +25,30 @@ _MODEL_TOOLS_DENYLIST = frozenset(
}
)
# Legacy export: some modules still import TOOL_FACTORIES. Tools are now intentionally
# restricted to a single safe builtin (`system_time`), so this is left empty.
TOOL_FACTORIES: tuple[object, ...] = ()
def _is_truthy(v: str | None) -> bool:
return str(v or "").strip().lower() in ("1", "true", "yes", "on")
def _skill_toolcall_enabled(store: SqliteStore | None) -> bool:
def _plugin_tools_enabled(store: Any | None = None) -> bool:
"""Admin setting ``AIA_ENABLE_PLUGIN_TOOLS`` wins; env aliases accepted.
Default ON when unset (matches historical ``AIA_PLUGIN_TOOLS_ENABLED=1``).
"""
if store is not None:
try:
raw = store.get_setting("AIA_ENABLE_PLUGIN_TOOLS")
if raw is not None and str(raw).strip() != "":
return _is_truthy(str(raw))
except Exception:
pass
for key in ("AIA_ENABLE_PLUGIN_TOOLS", "AIA_PLUGIN_TOOLS_ENABLED"):
if key in os.environ:
return _is_truthy(os.getenv(key))
return True
def _skill_toolcall_enabled(store: Any | None) -> bool:
try:
raw_env = str(os.getenv("AIA_SKILL_TOOLCALL_ENABLED") or "").strip()
if raw_env:
@ -232,8 +247,8 @@ def materialize_tool_specs(
except Exception as exc:
logger.warning("mcp tool load skipped: %s", exc)
# collect: plugin
if not _is_truthy(os.getenv("AIA_PLUGIN_TOOLS_ENABLED", "1")):
# collect: plugin (Admin AIA_ENABLE_PLUGIN_TOOLS / env alias AIA_PLUGIN_TOOLS_ENABLED)
if not _plugin_tools_enabled(store):
return _resolve_tool_conflicts(collected)
try:

View file

@ -262,7 +262,7 @@ def materialize_mcp_tools_for_specialist(
except Exception:
raw_allowed = ""
if not raw_allowed:
raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,manager").strip()
raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,manager,ops").strip()
allowed = {x.strip().lower() for x in raw_allowed.split(",") if x.strip()}
if binding_server_ids is None and sp and sp not in allowed:
return []