fix(trigger): require names for trigger creation
This commit is contained in:
@@ -16,7 +16,7 @@ These commands work inside chat channels and interactive agent sessions:
|
||||
| `/dream-restore` | List recent Dream memory versions |
|
||||
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
||||
| `/skill` | List enabled skills and their descriptions |
|
||||
| `/trigger` | Create a local external trigger for the current chat/session |
|
||||
| `/trigger` | Show external trigger usage |
|
||||
| `/trigger <name>` | Create a named local external trigger for the current chat/session |
|
||||
| `/pairing` | List pending pairing requests |
|
||||
| `/pairing approve <code>` | Approve a pairing code |
|
||||
@@ -59,8 +59,9 @@ Preset names come from the top-level `modelPresets` config. Switching is runtime
|
||||
|
||||
## External Triggers
|
||||
|
||||
Use `/trigger` when a local script or another service should be able to send a
|
||||
message into the current chat/session later.
|
||||
Use `/trigger <name>` when a local script or another service should be able to
|
||||
send a message into the current chat/session later. A name is required; plain
|
||||
`/trigger` only shows the usage hint.
|
||||
|
||||
Create the trigger from the chat where future messages should arrive:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Use this page when you know what you want to run and need the command shape. For
|
||||
| Send one test message | `nanobot agent -m "Hello!"` | First proof that install, config, provider, model, and workspace all work |
|
||||
| Chat in the terminal | `nanobot agent` | Interactive local chat; exit with `exit`, `/exit`, `:q`, or `Ctrl+D` |
|
||||
| Use WebUI or chat apps | `nanobot gateway` | Keep this terminal running, or use `nanobot gateway --background` |
|
||||
| Deliver a local external trigger | `nanobot trigger <id> "message"` | Created first with `/trigger` in the target chat/session |
|
||||
| Deliver a local external trigger | `nanobot trigger <id> "message"` | Created first with `/trigger <name>` in the target chat/session |
|
||||
| Serve an OpenAI-compatible API | `nanobot serve` | Starts `/v1/chat/completions`, `/v1/models`, and `/health` |
|
||||
| Check chat channel setup | `nanobot channels status` | Useful before starting `nanobot gateway` |
|
||||
| Log in to QR/OAuth-style channels | `nanobot channels login <channel>` | Used by channels such as WhatsApp and WeChat |
|
||||
@@ -126,7 +126,7 @@ The bundled WebUI is served by the WebSocket channel, usually on port `8765`, no
|
||||
## Local Triggers
|
||||
|
||||
`nanobot trigger` delivers one local message to a trigger that was created from
|
||||
a chat/session with `/trigger [name]`.
|
||||
a chat/session with `/trigger <name>`.
|
||||
|
||||
```bash
|
||||
nanobot trigger trg_8K4P2Q9X "Review PR #4502"
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ protected heartbeat system job. They run as scheduled turns in their origin
|
||||
chat/session and normally deliver the result back to that channel.
|
||||
|
||||
External triggers are also session-bound, but they do not have their own
|
||||
schedule. Create one from the target chat with `/trigger [name]`, then call
|
||||
schedule. Create one from the target chat with `/trigger <name>`, then call
|
||||
`nanobot trigger <id> "<message>"` when a local script or external service wants
|
||||
nanobot to respond in that session. Webhook servers, third-party auth, and
|
||||
event-to-message formatting stay outside nanobot.
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ There are two user-facing automation types:
|
||||
|
||||
- Scheduled automations, created by the agent's cron tool, run at a time,
|
||||
interval, or cron expression.
|
||||
- External triggers, created with `/trigger [name]`, run when you call a local
|
||||
- External triggers, created with `/trigger <name>`, run when you call a local
|
||||
command such as `nanobot trigger trg_8K4P2Q9X "Review PR #4502"`.
|
||||
|
||||
If a GitHub webhook, CI system, or another service should wake nanobot up, keep
|
||||
|
||||
@@ -218,13 +218,13 @@ if DISCORD_AVAILABLE:
|
||||
command_text = f"/model {preset}" if preset else "/model"
|
||||
await self._forward_slash_command(interaction, command_text)
|
||||
|
||||
@self.tree.command(name="trigger", description="Create a local trigger for this chat")
|
||||
@app_commands.describe(name="Optional trigger name")
|
||||
@self.tree.command(name="trigger", description="Create a named local trigger for this chat")
|
||||
@app_commands.describe(name="Trigger name")
|
||||
async def trigger_command(
|
||||
interaction: discord.Interaction,
|
||||
name: str | None = None,
|
||||
name: str,
|
||||
) -> None:
|
||||
name = (name or "").strip()
|
||||
name = name.strip()
|
||||
command_text = f"/trigger {name}" if name else "/trigger"
|
||||
await self._forward_slash_command(interaction, command_text)
|
||||
|
||||
|
||||
@@ -411,7 +411,7 @@ class TelegramChannel(BaseChannel):
|
||||
BotCommand("status", "Show bot status"),
|
||||
BotCommand("history", "Show recent conversation messages"),
|
||||
BotCommand("goal", "Start a sustained objective (long-running task)"),
|
||||
BotCommand("trigger", "Create a local trigger for this chat"),
|
||||
BotCommand("trigger", "Create a named local trigger"),
|
||||
BotCommand("pairing", "Manage DM pairing (approve/deny/list)"),
|
||||
BotCommand("model", "Switch runtime model preset"),
|
||||
BotCommand("skill", "List enabled skills"),
|
||||
|
||||
@@ -83,10 +83,10 @@ BUILTIN_COMMAND_SPECS: tuple[BuiltinCommandSpec, ...] = (
|
||||
),
|
||||
BuiltinCommandSpec(
|
||||
"/trigger",
|
||||
"Create local trigger",
|
||||
"Create a CLI trigger bound to this chat session.",
|
||||
"Create named local trigger",
|
||||
"Create a named CLI trigger bound to this chat session.",
|
||||
"zap",
|
||||
"[name]",
|
||||
"<name>",
|
||||
),
|
||||
BuiltinCommandSpec(
|
||||
"/dream",
|
||||
@@ -728,6 +728,18 @@ async def cmd_skill(ctx: CommandContext) -> OutboundMessage:
|
||||
|
||||
async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
"""Create a local trigger bound to the current session."""
|
||||
name = ctx.args.strip()
|
||||
if not name:
|
||||
return OutboundMessage(
|
||||
channel=ctx.msg.channel,
|
||||
chat_id=ctx.msg.chat_id,
|
||||
content=(
|
||||
"Usage: /trigger <name>\n\n"
|
||||
"Create a named local trigger bound to this chat session."
|
||||
),
|
||||
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
|
||||
)
|
||||
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
|
||||
loop = ctx.loop
|
||||
@@ -741,7 +753,6 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
if store is None:
|
||||
store = ExternalTriggerStore(workspace)
|
||||
|
||||
name = ctx.args.strip() or "External trigger"
|
||||
trigger = store.create(
|
||||
name=name,
|
||||
channel=ctx.msg.channel,
|
||||
|
||||
@@ -867,7 +867,7 @@ async def test_slash_new_is_blocked_for_disallowed_user() -> None:
|
||||
assert handled == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("slash_name", ["stop", "restart", "status", "history", "model", "trigger"])
|
||||
@pytest.mark.parametrize("slash_name", ["stop", "restart", "status", "history", "model"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_slash_commands_forward_via_handle_message(slash_name: str) -> None:
|
||||
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
||||
@@ -919,7 +919,7 @@ async def test_slash_model_forwards_optional_preset() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slash_trigger_forwards_optional_name() -> None:
|
||||
async def test_slash_trigger_forwards_required_name() -> None:
|
||||
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
||||
handled: list[dict] = []
|
||||
|
||||
|
||||
@@ -1577,6 +1577,7 @@ def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
|
||||
assert pat.fullmatch("/history")
|
||||
assert pat.fullmatch("/history 5")
|
||||
assert pat.fullmatch("/goal ship the feature")
|
||||
assert pat.fullmatch("/trigger")
|
||||
assert pat.fullmatch("/trigger PR review")
|
||||
assert pat.fullmatch("/pairing list")
|
||||
assert pat.fullmatch("/model fast")
|
||||
|
||||
@@ -45,5 +45,33 @@ async def test_trigger_command_creates_session_bound_local_trigger(tmp_path: Pat
|
||||
assert f"nanobot trigger {trigger.id} \"message\"" in response.content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_command_without_name_returns_usage_only(tmp_path: Path) -> None:
|
||||
router = CommandRouter()
|
||||
register_builtin_commands(router)
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, external_trigger_store=store)
|
||||
msg = InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="user",
|
||||
chat_id="chat-1",
|
||||
content="/trigger@nanobot_bot",
|
||||
metadata={"webui": True},
|
||||
)
|
||||
ctx = CommandContext(
|
||||
msg=msg,
|
||||
session=None,
|
||||
key="websocket:chat-1",
|
||||
raw="/trigger@nanobot_bot",
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
response = await router.dispatch(ctx)
|
||||
|
||||
assert response is not None
|
||||
assert "Usage: /trigger <name>" in response.content
|
||||
assert store.list_for_session("websocket:chat-1") == []
|
||||
|
||||
|
||||
def test_trigger_command_is_in_help_text() -> None:
|
||||
assert "/trigger [name]" in build_help_text()
|
||||
assert "/trigger <name>" in build_help_text()
|
||||
|
||||
Reference in New Issue
Block a user