refactor(trigger): name CLI trigger source as local

maintainer edit: cron is also a trigger source, so keep the new CLI-delivered source explicitly named as local trigger across backend, WebUI, docs, and tests.
This commit is contained in:
chengyongru
2026-07-02 13:32:46 +08:00
committed by Xubin Ren
parent 1ed2c9a213
commit 2ebf5c4972
40 changed files with 353 additions and 253 deletions
+3 -3
View File
@@ -68,7 +68,7 @@ class ChannelManager:
*,
session_manager: "SessionManager | None" = None,
cron_service: Any | None = None,
external_trigger_store: Any | None = None,
local_trigger_store: Any | None = None,
webui_runtime_model_name: Callable[[], str | None] | None = None,
webui_cron_pending_job_ids: Callable[[str], set[str]] | None = None,
webui_static_dist: bool = True,
@@ -79,7 +79,7 @@ class ChannelManager:
self.bus = bus
self._session_manager = session_manager
self._cron_service = cron_service
self._external_trigger_store = external_trigger_store
self._local_trigger_store = local_trigger_store
self._webui_runtime_model_name = webui_runtime_model_name
self._webui_cron_pending_job_ids = webui_cron_pending_job_ids
self._webui_static_dist = webui_static_dist
@@ -141,7 +141,7 @@ class ChannelManager:
runtime_surface=self._webui_runtime_surface,
runtime_capabilities_overrides=self._webui_runtime_capabilities,
cron_service=self._cron_service,
external_trigger_store=self._external_trigger_store,
local_trigger_store=self._local_trigger_store,
cron_pending_job_ids=self._webui_cron_pending_job_ids,
logger=logger,
)
+10 -10
View File
@@ -772,8 +772,8 @@ def trigger(
config: str | None = typer.Option(None, "--config", "-c", help="Config file path"),
):
"""Deliver a local trigger message to its bound chat session."""
from nanobot.triggers.store import (
ExternalTriggerStore,
from nanobot.triggers.local_store import (
LocalTriggerStore,
TriggerDisabledError,
TriggerNotFoundError,
TriggerStoreError,
@@ -781,7 +781,7 @@ def trigger(
runtime_config = _load_runtime_config(config, workspace)
content = _read_trigger_cli_message(message)
store = ExternalTriggerStore(runtime_config.workspace_path)
store = LocalTriggerStore(runtime_config.workspace_path)
try:
delivery = store.enqueue(trigger_id, content)
except (TriggerNotFoundError, TriggerDisabledError) as exc:
@@ -909,8 +909,8 @@ def _run_gateway(
from nanobot.providers.image_generation import image_gen_provider_configs
from nanobot.session.manager import SessionManager
from nanobot.session.webui_turns import WebuiTurnCoordinator
from nanobot.triggers.runner import run_external_trigger_queue
from nanobot.triggers.store import ExternalTriggerStore
from nanobot.triggers.local_runner import run_local_trigger_queue
from nanobot.triggers.local_store import LocalTriggerStore
from nanobot.webui.token_usage import TokenUsageHook
port = port if port is not None else config.gateway.port
@@ -933,7 +933,7 @@ def _run_gateway(
# Create cron service with workspace-scoped store
cron_store_path = config.workspace_path / "cron" / "jobs.json"
cron = CronService(cron_store_path)
trigger_store = ExternalTriggerStore(config.workspace_path)
trigger_store = LocalTriggerStore(config.workspace_path)
# Create agent with cron service
agent = AgentLoop.from_config(
@@ -954,7 +954,7 @@ def _run_gateway(
sessions=session_manager,
schedule_background=lambda coro: agent._schedule_background(coro),
).subscribe(runtime_events)
agent.external_trigger_store = trigger_store
agent.local_trigger_store = trigger_store
from nanobot.bus.events import OutboundMessage
from nanobot.session.keys import session_key_for_channel
@@ -1151,7 +1151,7 @@ def _run_gateway(
bus,
session_manager=session_manager,
cron_service=cron,
external_trigger_store=trigger_store,
local_trigger_store=trigger_store,
webui_runtime_model_name=_webui_runtime_model_name,
webui_cron_pending_job_ids=getattr(agent, "pending_cron_job_ids_for_session", None),
webui_static_dist=webui_static_dist,
@@ -1295,8 +1295,8 @@ def _run_gateway(
asyncio.create_task(agent.run(), name="nanobot-agent-loop"),
asyncio.create_task(channels.start_all(), name="nanobot-channels"),
asyncio.create_task(
run_external_trigger_queue(store=trigger_store, bus=bus),
name="nanobot-external-triggers",
run_local_trigger_queue(store=trigger_store, bus=bus),
name="nanobot-local-triggers",
),
]
if health_server_enabled:
+3 -3
View File
@@ -740,7 +740,7 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
)
from nanobot.triggers.store import ExternalTriggerStore
from nanobot.triggers.local_store import LocalTriggerStore
loop = ctx.loop
workspace = getattr(loop, "workspace", None)
@@ -749,9 +749,9 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
if workspace is None:
raise RuntimeError("workspace unavailable for trigger creation")
store = getattr(loop, "external_trigger_store", None)
store = getattr(loop, "local_trigger_store", None)
if store is None:
store = ExternalTriggerStore(workspace)
store = LocalTriggerStore(workspace)
trigger = store.create(
name=name,
+5 -3
View File
@@ -56,9 +56,9 @@ def automation_history_overrides_for_spec(
def _automation_specs() -> tuple[AutomationTurnSpec, ...]:
# Source modules import the generic helpers above, so keep spec loading lazy.
from nanobot.cron.session_turns import CRON_AUTOMATION_SPEC
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_AUTOMATION_SPEC
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_AUTOMATION_SPEC
return (CRON_AUTOMATION_SPEC, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
return (CRON_AUTOMATION_SPEC, LOCAL_TRIGGER_AUTOMATION_SPEC)
def automation_history_overrides(
@@ -87,4 +87,6 @@ def is_automation_history_message(message: Mapping[str, Any] | None) -> bool:
def is_automation_kind(value: Any) -> bool:
return isinstance(value, str) and any(spec.kind == value for spec in _automation_specs())
return isinstance(value, str) and (
value == "trigger" or any(spec.kind == value for spec in _automation_specs())
)
+6 -6
View File
@@ -1,16 +1,16 @@
"""Local external trigger support."""
"""Local trigger support."""
from nanobot.triggers.store import (
ExternalTriggerStore,
from nanobot.triggers.local_store import (
LocalTriggerStore,
TriggerDisabledError,
TriggerNotFoundError,
TriggerStoreError,
)
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
__all__ = [
"ExternalTrigger",
"ExternalTriggerStore",
"LocalTrigger",
"LocalTriggerStore",
"TriggerDelivery",
"TriggerDisabledError",
"TriggerNotFoundError",
@@ -1,4 +1,4 @@
"""Gateway delivery loop for local external triggers."""
"""Gateway delivery loop for local triggers."""
from __future__ import annotations
@@ -10,21 +10,21 @@ from loguru import logger
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_META
from nanobot.triggers.store import ExternalTriggerStore
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META
from nanobot.triggers.local_store import LocalTriggerStore
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
async def run_external_trigger_queue(
async def run_local_trigger_queue(
*,
store: ExternalTriggerStore,
store: LocalTriggerStore,
bus: MessageBus,
poll_interval_s: float = 0.5,
batch_size: int = 20,
) -> None:
"""Poll local trigger deliveries and publish them as normal inbound messages."""
logger.info("External trigger queue started")
logger.info("Local trigger queue started")
recovered = store.recover_processing_deliveries()
if recovered:
logger.warning(
@@ -80,7 +80,7 @@ class _TerminalDeliveryError(RuntimeError):
async def _publish_delivery(
store: ExternalTriggerStore,
store: LocalTriggerStore,
bus: MessageBus,
delivery: TriggerDelivery,
) -> None:
@@ -107,9 +107,9 @@ async def _publish_delivery(
)
def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
def _delivery_metadata(trigger: LocalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
metadata = dict(trigger.origin_metadata or {})
metadata[EXTERNAL_TRIGGER_META] = {
metadata[LOCAL_TRIGGER_META] = {
"trigger_id": trigger.id,
"trigger_name": trigger.name,
"delivery_id": delivery.id,
@@ -118,7 +118,7 @@ def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> d
if trigger.channel == "websocket":
metadata.pop(WEBUI_TURN_METADATA_KEY, None)
metadata[WEBUI_TURN_METADATA_KEY] = f"trigger:{trigger.id}:{uuid.uuid4().hex}"
source: dict[str, str] = {"kind": "trigger"}
source: dict[str, str] = {"kind": "local_trigger"}
if trigger.name:
source["label"] = trigger.name
metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source
+51
View File
@@ -0,0 +1,51 @@
"""Shared metadata helpers for local trigger session turns."""
from __future__ import annotations
from typing import Any, Mapping
from nanobot.session.automation_turns import (
AutomationTurnSpec,
automation_history_overrides_for_spec,
automation_trigger,
)
LOCAL_TRIGGER_META = "_local_trigger"
def _local_trigger_history_text(trigger: Mapping[str, Any]) -> str:
name = trigger.get("trigger_name")
trigger_id = trigger.get("trigger_id")
label = name if isinstance(name, str) and name.strip() else trigger_id
return (
f"Local trigger received: {label}"
if isinstance(label, str) and label.strip()
else "Local trigger received"
)
LOCAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
kind="local_trigger",
trigger_meta_key=LOCAL_TRIGGER_META,
history_fields={
"trigger_id": "trigger_id",
"trigger_name": "trigger_name",
"trigger_delivery_id": "delivery_id",
},
text_builder=_local_trigger_history_text,
)
def local_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""Return structured local trigger metadata when present."""
return automation_trigger(metadata, LOCAL_TRIGGER_AUTOMATION_SPEC)
def local_trigger_history_overrides(
metadata: Mapping[str, Any] | None,
) -> tuple[str | None, dict[str, Any]]:
"""Return session-history text/metadata overrides for a local trigger turn."""
return automation_history_overrides_for_spec(
metadata,
LOCAL_TRIGGER_AUTOMATION_SPEC,
)
@@ -14,7 +14,7 @@ from typing import Any
from filelock import FileLock
from loguru import logger
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
_TRIGGER_ID_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
_MAX_RUN_HISTORY = 20
@@ -34,7 +34,7 @@ class TriggerDisabledError(TriggerStoreError):
"""Raised when a trigger is disabled."""
class ExternalTriggerStore:
class LocalTriggerStore:
"""Persistent local triggers for one workspace."""
def __init__(self, workspace_path: Path):
@@ -55,8 +55,8 @@ class ExternalTriggerStore:
session_key: str,
sender_id: str = "trigger",
origin_metadata: dict[str, Any] | None = None,
) -> ExternalTrigger:
"""Create a new session-bound external trigger."""
) -> LocalTrigger:
"""Create a new session-bound local trigger."""
clean_name = _clean_name(name)
channel = channel.strip()
chat_id = chat_id.strip()
@@ -70,7 +70,7 @@ class ExternalTriggerStore:
triggers = self._load_triggers_unlocked()
existing_ids = {trigger.id for trigger in triggers}
trigger_id = _new_trigger_id(existing_ids)
trigger = ExternalTrigger(
trigger = LocalTrigger(
id=trigger_id,
name=clean_name,
enabled=True,
@@ -86,7 +86,7 @@ class ExternalTriggerStore:
self._save_triggers_unlocked(triggers)
return trigger
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]:
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]:
"""List triggers in this workspace."""
self._ensure_dirs()
with self._lock:
@@ -100,7 +100,7 @@ class ExternalTriggerStore:
session_key: str,
*,
include_disabled: bool = True,
) -> list[ExternalTrigger]:
) -> list[LocalTrigger]:
"""List triggers bound to one session key."""
return [
trigger
@@ -108,13 +108,13 @@ class ExternalTriggerStore:
if trigger.session_key == session_key
]
def get(self, trigger_id: str) -> ExternalTrigger | None:
def get(self, trigger_id: str) -> LocalTrigger | None:
"""Return one trigger by ID."""
self._ensure_dirs()
with self._lock:
return self._find_unlocked(self._load_triggers_unlocked(), trigger_id)
def enable(self, trigger_id: str, *, enabled: bool) -> ExternalTrigger | None:
def enable(self, trigger_id: str, *, enabled: bool) -> LocalTrigger | None:
"""Enable or disable a trigger."""
self._ensure_dirs()
with self._lock:
@@ -127,7 +127,7 @@ class ExternalTriggerStore:
self._save_triggers_unlocked(triggers)
return trigger
def update(self, trigger_id: str, *, name: str | None = None) -> ExternalTrigger | None:
def update(self, trigger_id: str, *, name: str | None = None) -> LocalTrigger | None:
"""Update mutable trigger fields."""
self._ensure_dirs()
with self._lock:
@@ -267,13 +267,13 @@ class ExternalTriggerStore:
self.processing_dir.mkdir(parents=True, exist_ok=True)
self.failed_dir.mkdir(parents=True, exist_ok=True)
def _load_triggers_unlocked(self) -> list[ExternalTrigger]:
def _load_triggers_unlocked(self) -> list[LocalTrigger]:
if not self.store_path.exists():
return []
try:
data = json.loads(self.store_path.read_text(encoding="utf-8"))
return [
ExternalTrigger.from_dict(raw)
LocalTrigger.from_dict(raw)
for raw in data.get("triggers", [])
if isinstance(raw, dict)
]
@@ -288,7 +288,7 @@ class ExternalTriggerStore:
"as a .corrupt-<ts> backup"
) from exc
def _save_triggers_unlocked(self, triggers: list[ExternalTrigger]) -> None:
def _save_triggers_unlocked(self, triggers: list[LocalTrigger]) -> None:
payload = {
"version": 1,
"triggers": [trigger.to_dict() for trigger in triggers],
@@ -297,9 +297,9 @@ class ExternalTriggerStore:
@staticmethod
def _find_unlocked(
triggers: list[ExternalTrigger],
triggers: list[LocalTrigger],
trigger_id: str,
) -> ExternalTrigger | None:
) -> LocalTrigger | None:
return next((trigger for trigger in triggers if trigger.id == trigger_id), None)
def _move_bad_delivery_unlocked(self, path: Path) -> None:
@@ -356,7 +356,7 @@ def _new_trigger_id(existing_ids: set[str]) -> str:
def _clean_name(name: str) -> str:
stripped = " ".join(name.strip().split())
return (stripped or "External trigger")[:120]
return (stripped or "Local trigger")[:120]
def _now_ms() -> int:
@@ -1,4 +1,4 @@
"""Persistent types for local external triggers."""
"""Persistent types for local triggers."""
from __future__ import annotations
@@ -40,7 +40,7 @@ class TriggerRunRecord:
@dataclass
class ExternalTrigger:
class LocalTrigger:
"""A session-bound local trigger."""
id: str
@@ -59,7 +59,7 @@ class ExternalTrigger:
run_history: list[TriggerRunRecord] = field(default_factory=list)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ExternalTrigger":
def from_dict(cls, data: dict[str, Any]) -> "LocalTrigger":
history = [
record if isinstance(record, TriggerRunRecord) else TriggerRunRecord.from_dict(record)
for record in data.get("runHistory", data.get("run_history", []))
-51
View File
@@ -1,51 +0,0 @@
"""Shared metadata helpers for local external trigger session turns."""
from __future__ import annotations
from typing import Any, Mapping
from nanobot.session.automation_turns import (
AutomationTurnSpec,
automation_history_overrides_for_spec,
automation_trigger,
)
EXTERNAL_TRIGGER_META = "_external_trigger"
def _external_trigger_history_text(trigger: Mapping[str, Any]) -> str:
name = trigger.get("trigger_name")
trigger_id = trigger.get("trigger_id")
label = name if isinstance(name, str) and name.strip() else trigger_id
return (
f"External trigger received: {label}"
if isinstance(label, str) and label.strip()
else "External trigger received"
)
EXTERNAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
kind="trigger",
trigger_meta_key=EXTERNAL_TRIGGER_META,
history_fields={
"trigger_id": "trigger_id",
"trigger_name": "trigger_name",
"trigger_delivery_id": "delivery_id",
},
text_builder=_external_trigger_history_text,
)
def external_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""Return structured external trigger metadata when present."""
return automation_trigger(metadata, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
def external_trigger_history_overrides(
metadata: Mapping[str, Any] | None,
) -> tuple[str | None, dict[str, Any]]:
"""Return session-history text/metadata overrides for an external trigger turn."""
return automation_history_overrides_for_spec(
metadata,
EXTERNAL_TRIGGER_AUTOMATION_SPEC,
)
+4 -4
View File
@@ -26,7 +26,7 @@ class GatewayServices:
workspaces: WebUIWorkspaceController
session_manager: Any | None
cron_service: Any | None
external_trigger_store: Any | None
local_trigger_store: Any | None
cron_pending_job_ids: Callable[[str], set[str]] | None
@@ -43,7 +43,7 @@ def build_gateway_services(
runtime_capabilities_overrides: dict[str, Any] | None,
disabled_skills: set[str] | None = None,
cron_service: Any | None = None,
external_trigger_store: Any | None = None,
local_trigger_store: Any | None = None,
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
logger: Any = default_logger,
) -> GatewayServices:
@@ -72,7 +72,7 @@ def build_gateway_services(
skills_workspace_path=workspace_path,
disabled_skills=disabled_skills,
cron_service=cron_service,
external_trigger_store=external_trigger_store,
local_trigger_store=local_trigger_store,
cron_pending_job_ids=cron_pending_job_ids,
log=logger,
)
@@ -84,6 +84,6 @@ def build_gateway_services(
workspaces=workspaces,
session_manager=session_manager,
cron_service=cron_service,
external_trigger_store=external_trigger_store,
local_trigger_store=local_trigger_store,
cron_pending_job_ids=cron_pending_job_ids,
)
+19 -19
View File
@@ -8,9 +8,9 @@ from typing import Any, Protocol
from nanobot.cron.types import CronJob
from nanobot.session.automation_turns import is_automation_history_message
from nanobot.session.manager import _message_preview_text
from nanobot.triggers.types import ExternalTrigger
from nanobot.triggers.local_types import LocalTrigger
AutomationJob = CronJob | ExternalTrigger
AutomationJob = CronJob | LocalTrigger
class _CronServiceLike(Protocol):
@@ -24,15 +24,15 @@ class _CronServiceLike(Protocol):
) -> list[CronJob]: ...
class _ExternalTriggerStoreLike(Protocol):
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]: ...
class _LocalTriggerStoreLike(Protocol):
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]: ...
def list_for_session(
self,
session_key: str,
*,
include_disabled: bool = True,
) -> list[ExternalTrigger]: ...
) -> list[LocalTrigger]: ...
class _SessionManagerLike(Protocol):
@@ -43,7 +43,7 @@ def session_automation_jobs(
cron_service: _CronServiceLike | None,
session_key: str,
*,
external_trigger_store: _ExternalTriggerStoreLike | None = None,
local_trigger_store: _LocalTriggerStoreLike | None = None,
) -> list[AutomationJob]:
"""Return user automations attached to the WebUI session."""
jobs: list[AutomationJob] = []
@@ -54,9 +54,9 @@ def session_automation_jobs(
include_disabled=True,
)
)
if external_trigger_store is not None:
if local_trigger_store is not None:
jobs.extend(
external_trigger_store.list_for_session(
local_trigger_store.list_for_session(
session_key,
include_disabled=True,
)
@@ -68,7 +68,7 @@ def session_automations_payload(
cron_service: _CronServiceLike | None,
session_key: str,
*,
external_trigger_store: _ExternalTriggerStoreLike | None = None,
local_trigger_store: _LocalTriggerStoreLike | None = None,
pending_job_ids: Collection[str] | None = None,
) -> dict[str, Any]:
"""Return user-created automation jobs attached to a WebUI session."""
@@ -77,7 +77,7 @@ def session_automations_payload(
session_automation_jobs(
cron_service,
session_key,
external_trigger_store=external_trigger_store,
local_trigger_store=local_trigger_store,
),
pending_job_ids=pending_job_ids,
)
@@ -87,7 +87,7 @@ def session_automations_payload(
def all_automations_payload(
cron_service: _CronServiceLike | None,
*,
external_trigger_store: _ExternalTriggerStoreLike | None = None,
local_trigger_store: _LocalTriggerStoreLike | None = None,
session_manager: _SessionManagerLike | None = None,
pending_job_ids: Collection[str] | None = None,
) -> dict[str, Any]:
@@ -95,8 +95,8 @@ def all_automations_payload(
jobs: list[AutomationJob] = []
if cron_service is not None:
jobs.extend(cron_service.list_jobs(include_disabled=True))
if external_trigger_store is not None:
jobs.extend(external_trigger_store.list_triggers(include_disabled=True))
if local_trigger_store is not None:
jobs.extend(local_trigger_store.list_triggers(include_disabled=True))
return {
"jobs": serialize_automation_jobs(
jobs,
@@ -132,7 +132,7 @@ def _serialize_job(
include_details: bool = False,
session_manager: _SessionManagerLike | None = None,
) -> dict[str, Any]:
if isinstance(job, ExternalTrigger):
if isinstance(job, LocalTrigger):
return _serialize_trigger(
job,
include_details=include_details,
@@ -187,7 +187,7 @@ def _serialize_job(
def _serialize_trigger(
trigger: ExternalTrigger,
trigger: LocalTrigger,
*,
include_details: bool = False,
session_manager: _SessionManagerLike | None = None,
@@ -197,16 +197,16 @@ def _serialize_trigger(
"id": trigger.id,
"name": trigger.name,
"enabled": trigger.enabled,
"kind": "external_trigger",
"kind": "local_trigger",
"schedule": {
"kind": "external",
"kind": "local",
"at_ms": None,
"every_ms": None,
"expr": None,
"tz": None,
},
"payload": {
"kind": "external_trigger",
"kind": "local_trigger",
"message": command,
"command": command,
},
@@ -273,7 +273,7 @@ def _origin_payload(
def _trigger_origin_payload(
trigger: ExternalTrigger,
trigger: LocalTrigger,
session_manager: _SessionManagerLike | None,
) -> dict[str, Any] | None:
channel = trigger.channel
+24 -24
View File
@@ -26,7 +26,7 @@ from websockets.http11 import Response
from nanobot.command.builtin import builtin_command_palette
from nanobot.cron.session_turns import is_bound_cron_job
from nanobot.cron.types import CronJob, CronSchedule
from nanobot.triggers.types import ExternalTrigger
from nanobot.triggers.local_types import LocalTrigger
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload
@@ -90,7 +90,7 @@ if TYPE_CHECKING:
from nanobot.bus.queue import MessageBus
from nanobot.cron.service import CronService
from nanobot.session.manager import SessionManager
from nanobot.triggers.store import ExternalTriggerStore
from nanobot.triggers.local_store import LocalTriggerStore
def _decode_api_key(raw_key: str) -> str | None:
@@ -155,7 +155,7 @@ class GatewayHTTPHandler:
skills_workspace_path: Path,
disabled_skills: set[str] | None = None,
cron_service: CronService | None = None,
external_trigger_store: ExternalTriggerStore | None = None,
local_trigger_store: LocalTriggerStore | None = None,
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
log: Any = logger,
) -> None:
@@ -170,7 +170,7 @@ class GatewayHTTPHandler:
self.skills_workspace_path = skills_workspace_path
self.disabled_skills = disabled_skills or set()
self.cron_service = cron_service
self.external_trigger_store = external_trigger_store
self.local_trigger_store = local_trigger_store
self.cron_pending_job_ids = cron_pending_job_ids
self._log = log
self._runtime_surface = runtime_surface
@@ -494,7 +494,7 @@ class GatewayHTTPHandler:
session_automations_payload(
self.cron_service,
decoded_key,
external_trigger_store=self.external_trigger_store,
local_trigger_store=self.local_trigger_store,
pending_job_ids=pending_job_ids,
)
)
@@ -514,7 +514,7 @@ class GatewayHTTPHandler:
automation_jobs = session_automation_jobs(
self.cron_service,
decoded_key,
external_trigger_store=self.external_trigger_store,
local_trigger_store=self.local_trigger_store,
)
if automation_jobs and delete_automations not in {"1", "true", "yes"}:
return _http_json_response(
@@ -526,9 +526,9 @@ class GatewayHTTPHandler:
)
if automation_jobs:
for job in automation_jobs:
if isinstance(job, ExternalTrigger):
if self.external_trigger_store is not None:
self.external_trigger_store.delete(job.id)
if isinstance(job, LocalTrigger):
if self.local_trigger_store is not None:
self.local_trigger_store.delete(job.id)
elif self.cron_service is not None:
self.cron_service.remove_job(job.id)
deleted = self.session_manager.delete_session(decoded_key)
@@ -567,7 +567,7 @@ class GatewayHTTPHandler:
return _http_json_response(
all_automations_payload(
self.cron_service,
external_trigger_store=self.external_trigger_store,
local_trigger_store=self.local_trigger_store,
session_manager=self.session_manager,
pending_job_ids=self._pending_cron_job_ids_for_all(),
)
@@ -580,16 +580,16 @@ class GatewayHTTPHandler:
) -> Response:
if not self.check_api_token(request):
return _http_error(401, "Unauthorized")
if self.cron_service is None and self.external_trigger_store is None:
if self.cron_service is None and self.local_trigger_store is None:
return _http_error(503, "automation service unavailable")
query = _parse_query(request.path)
job_id = (_query_first(query, "id") or _query_first(query, "job_id") or "").strip()
if not job_id:
return _http_error(400, "missing automation id")
trigger = self.external_trigger_store.get(job_id) if self.external_trigger_store else None
trigger = self.local_trigger_store.get(job_id) if self.local_trigger_store else None
if trigger is not None:
return self._handle_external_trigger_action(request, action, trigger)
return self._handle_local_trigger_action(request, action, trigger)
if self.cron_service is None:
return _http_error(404, "automation not found")
@@ -638,34 +638,34 @@ class GatewayHTTPHandler:
return self._handle_webui_automations(request)
def _handle_external_trigger_action(
def _handle_local_trigger_action(
self,
request: WsRequest,
action: str,
trigger: ExternalTrigger,
trigger: LocalTrigger,
) -> Response:
if self.external_trigger_store is None:
if self.local_trigger_store is None:
return _http_error(503, "trigger service unavailable")
if action == "enable":
if self.external_trigger_store.enable(trigger.id, enabled=True) is None:
if self.local_trigger_store.enable(trigger.id, enabled=True) is None:
return _http_error(404, "automation not found")
elif action == "disable":
if self.external_trigger_store.enable(trigger.id, enabled=False) is None:
if self.local_trigger_store.enable(trigger.id, enabled=False) is None:
return _http_error(404, "automation not found")
elif action == "delete":
if not self.external_trigger_store.delete(trigger.id):
if not self.local_trigger_store.delete(trigger.id):
return _http_error(404, "automation not found")
elif action == "run":
return _http_error(409, "external trigger requires a CLI message")
return _http_error(409, "local trigger requires a CLI message")
elif action == "update":
values = _automation_values_from_request(request)
if values is None:
return _http_error(400, "invalid automation update payload")
parsed = _parse_external_trigger_update(values)
parsed = _parse_local_trigger_update(values)
if isinstance(parsed, str):
return _http_error(400, parsed)
if parsed:
if self.external_trigger_store.update(trigger.id, **parsed) is None:
if self.local_trigger_store.update(trigger.id, **parsed) is None:
return _http_error(404, "automation not found")
else:
return _http_error(404, "unknown automation action")
@@ -884,7 +884,7 @@ def _parse_automation_update(
return update
def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
def _parse_local_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
update: dict[str, Any] = {}
if "name" in values:
raw_name = values.get("name")
@@ -896,7 +896,7 @@ def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | s
update["name"] = name
forbidden = [key for key in ("message", "schedule") if key in values]
if forbidden:
return "external trigger updates only support name"
return "local trigger updates only support name"
return update