mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
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:
parent
e2fc72607e
commit
628a9dffd2
32 changed files with 127 additions and 870 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
"""Application-facing runtime entrypoints."""
|
||||
|
||||
|
|
@ -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)),
|
||||
)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.adapter import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.compat import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.gateway_adapter import * # noqa: F403
|
||||
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.manager import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.models import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.prompt_injector import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.state_store import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.switch import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.tool_policy import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.tool_specs import * # noqa: F403
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from runtime.plan_agent_v2.trace import * # noqa: F403
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue