mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 04:37:44 +02:00
[aidan] feat/agent-scheduling: route recurring asks through native workflows, deny claude cron skill
This commit is contained in:
@@ -612,6 +612,12 @@ class AgentManager:
|
||||
"mcp__openswarm-schedule__DeleteScheduledWorkflow",
|
||||
"mcp__openswarm-schedule__PauseAllWorkflows",
|
||||
}
|
||||
_CLAUDE_INTERNAL_SCHEDULER_TOOLS = ("CronCreate", "CronList", "CronDelete")
|
||||
|
||||
def _is_claude_schedule_skill(tool_name: str, tool_input) -> bool:
|
||||
if tool_name != "Skill" or not isinstance(tool_input, dict):
|
||||
return False
|
||||
return str(tool_input.get("skill") or "").strip().lower() == "schedule"
|
||||
|
||||
# OS-level scheduling across macOS/Linux/Windows. Agent must
|
||||
# not install cron entries, launchd plists, Windows scheduled
|
||||
@@ -732,6 +738,8 @@ class AgentManager:
|
||||
"""
|
||||
if tool_name == "Bash" and _looks_like_os_scheduling(tool_input):
|
||||
return "ask", None
|
||||
if tool_name in _CLAUDE_INTERNAL_SCHEDULER_TOOLS:
|
||||
return "deny", None
|
||||
# Committing or mutating a native recurring schedule is the in-app
|
||||
# twin of the crontab gate above: real, user-visible, hard-to-undo,
|
||||
# so it goes through ApprovalBar every time regardless of the
|
||||
@@ -907,6 +915,14 @@ class AgentManager:
|
||||
return decision
|
||||
|
||||
async def can_use_tool(tool_name, input_data, context):
|
||||
if _is_claude_schedule_skill(tool_name, input_data):
|
||||
p_note_tool_used(tool_name, False)
|
||||
return PermissionResultDeny(
|
||||
message=(
|
||||
"Use the openswarm-schedule MCP tools instead of "
|
||||
"Claude's internal schedule skill."
|
||||
)
|
||||
)
|
||||
sensitive_pattern: str | None = None
|
||||
if tool_name != "AskUserQuestion":
|
||||
policy, sensitive_pattern = _maybe_override_policy(
|
||||
@@ -936,6 +952,18 @@ class AgentManager:
|
||||
|
||||
if tool_name and tool_name != "AskUserQuestion":
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
if _is_claude_schedule_skill(tool_name, tool_input):
|
||||
p_note_tool_used(tool_name, False)
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": hook_event,
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": (
|
||||
"Use the openswarm-schedule MCP tools instead "
|
||||
"of Claude's internal schedule skill."
|
||||
),
|
||||
}
|
||||
}
|
||||
policy, sensitive_pattern = _maybe_override_policy(
|
||||
_get_effective_policy(tool_name), tool_name, tool_input
|
||||
)
|
||||
@@ -1225,21 +1253,35 @@ class AgentManager:
|
||||
|
||||
mcp_registry_ctx = self._build_mcp_registry_summary(session.allowed_tools, session.active_mcps)
|
||||
global_settings = load_settings()
|
||||
# Nudge the agent to surface ScheduleWorkflow proactively when
|
||||
# the user's ask looks recurring. The per-tool description
|
||||
# carries the full protocol; this is just the "when to think
|
||||
# about it" signal so the agent doesn't ignore the surface.
|
||||
# Nudge the agent to surface workflow conversion proactively when
|
||||
# the user's ask looks recurring. The actual conversion is owned
|
||||
# by the UI prompt; chat should not turn this into a cadence Q&A.
|
||||
schedule_ctx = (
|
||||
"<scheduling_guidance>\n"
|
||||
"After completing a substantive task, if the work looks "
|
||||
"repeatable (the user said 'every', 'each', 'daily', "
|
||||
"'weekly', 'morning', 'before standup', or you just did "
|
||||
"the same sequence twice in this session), offer to "
|
||||
"schedule it. Use AskUserQuestion to confirm cadence, "
|
||||
"then ScheduleWorkflow to create it. Never reach for "
|
||||
"crontab, launchctl, or schtasks; always use the native "
|
||||
"scheduler so the user can see, pause, and edit it. "
|
||||
"Don't ask after trivial one-off requests.\n"
|
||||
"When a normal chat produces work that would naturally be "
|
||||
"useful again later, prefer the openswarm-schedule MCP "
|
||||
"workflow path over Claude's internal scheduler. If the user "
|
||||
"explicitly says 'daily', 'weekly', 'every', 'each', "
|
||||
"'morning', 'before standup', 'monitor', 'alert me', or "
|
||||
"'keep this updated', treat that as a strong recurring-work "
|
||||
"signal. After completing that work, call "
|
||||
"SuggestConvertToWorkflow with a short reason and cadence "
|
||||
"hint so the UI can open its workflow-conversion prompt. You "
|
||||
"may say one short sentence before the tool call, such as "
|
||||
"'This is a good fit for built-in Workflows.' Do not repeat "
|
||||
"that advice after the tool call, do not send a follow-up "
|
||||
"assistant message like 'Done', do not say you are 'nudging' "
|
||||
"the UI, and do not mention the tool. If the user "
|
||||
"then says 'ok', 'yes', or 'do it', do not ask what time and "
|
||||
"do not schedule from chat; the UI prompt/Convert button owns "
|
||||
"the conversion flow. Only call ScheduleWorkflow when the user "
|
||||
"explicitly asks you to create a live schedule and has already "
|
||||
"given an exact cadence and time (for example, 'every weekday "
|
||||
"at 8am'). If cadence or time is missing, use "
|
||||
"SuggestConvertToWorkflow instead of asking a follow-up. Never "
|
||||
"use Skill('schedule'), CronCreate, CronList, CronDelete, "
|
||||
"crontab, launchctl, or schtasks for user-facing recurring "
|
||||
"work.\n"
|
||||
"</scheduling_guidance>"
|
||||
)
|
||||
composed_prompt = self._compose_system_prompt(
|
||||
@@ -1555,6 +1597,15 @@ class AgentManager:
|
||||
else:
|
||||
effective_allowed.append(f"mcp__{name}__*")
|
||||
|
||||
if "openswarm-schedule" in mcp_servers:
|
||||
effective_allowed = [
|
||||
t for t in effective_allowed
|
||||
if t not in _CLAUDE_INTERNAL_SCHEDULER_TOOLS
|
||||
]
|
||||
for _bt in _CLAUDE_INTERNAL_SCHEDULER_TOOLS:
|
||||
if _bt not in effective_disallowed:
|
||||
effective_disallowed.append(_bt)
|
||||
|
||||
# If the openswarm-web MCP was registered, the CLI's built-in
|
||||
# WebSearch/WebFetch are guaranteed to fail (no Anthropic
|
||||
# backend). Suppress them so the model picks our MCP variants
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
Why this exists: the agent should be able to schedule recurring work on
|
||||
the user's behalf, but ALWAYS through the native scheduler (visible,
|
||||
auditable, cost-capped) rather than `crontab`. Each tool is a thin
|
||||
wrapper around /api/workflows/*. The descriptions are written to nudge
|
||||
the agent toward AskUserQuestion-first behavior (confirm cadence with
|
||||
the user before calling ScheduleWorkflow).
|
||||
wrapper around /api/workflows/*. The descriptions are written to prefer
|
||||
UI-owned workflow conversion for vague recurring asks, and to reserve
|
||||
ScheduleWorkflow for exact, user-specified live schedules.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -52,8 +52,13 @@ TOOLS = [
|
||||
"name": "ScheduleWorkflow",
|
||||
"description": (
|
||||
"Create a recurring scheduled workflow for the user. Use this "
|
||||
"ONLY after confirming cadence with the user via AskUserQuestion "
|
||||
"(do not assume — the user must pick or accept the time). "
|
||||
"ONLY when the user explicitly asks you to create a live schedule "
|
||||
"and has already supplied an exact cadence and time. Do not use "
|
||||
"this after a generic convert-to-workflow suggestion, and do not "
|
||||
"ask follow-up questions like 'what time should it run' from a "
|
||||
"normal chat. If cadence or time is missing, call "
|
||||
"SuggestConvertToWorkflow instead so the UI can open the workflow "
|
||||
"conversion prompt. "
|
||||
"The workflow runs the listed steps on the schedule and is "
|
||||
"visible in the user's Workflows hub. Never use crontab, "
|
||||
"launchctl, or schtasks to schedule recurring work; always use "
|
||||
@@ -240,13 +245,25 @@ TOOLS = [
|
||||
{
|
||||
"name": "SuggestConvertToWorkflow",
|
||||
"description": (
|
||||
"Call this at the end of a response when you have just completed a task "
|
||||
"the user is likely to want to repeat on a schedule (e.g. a daily report, "
|
||||
"a weekly digest, a recurring data check, a monitoring ping). Do NOT call "
|
||||
"it for one-off tasks, debugging sessions, creative work, or anything "
|
||||
"where 'repeat it tomorrow' would be odd. Use sparingly — once per session "
|
||||
"maximum, only with high confidence. This nudges the frontend to highlight "
|
||||
"the 'Convert to Workflow' button and suggest a cadence."
|
||||
"Call this at the end of a response when the completed task is a clear "
|
||||
"candidate for repeatable scheduled work (e.g. a daily report, weekly "
|
||||
"digest, recurring data check, monitoring ping, inbox triage, or status "
|
||||
"briefing). Prefer this native workflow nudge over Claude's internal "
|
||||
"schedule skill or CronCreate/CronList/CronDelete tools. Use it whenever "
|
||||
"the user explicitly mentions daily, weekly, every, each, mornings, "
|
||||
"standup, monitoring, alerts, or keeping something updated, and when you "
|
||||
"have just done a sequence that would naturally be useful again later. Do "
|
||||
"NOT call it for one-off tasks, debugging sessions, creative work, or "
|
||||
"anything where 'repeat it tomorrow' would be odd. It is OK to call this "
|
||||
"more than once per session for distinct workflow candidates, but avoid "
|
||||
"repeated nudges for the same task. This nudges the frontend to highlight "
|
||||
"the 'Convert to Workflow' button and open the workflow-conversion "
|
||||
"prompt. In user-facing text, say at most one short sentence, such "
|
||||
"as: 'This is a good fit for built-in Workflows.' Do not repeat the "
|
||||
"advice after this tool returns, do not say you are nudging the UI, "
|
||||
"and do not ask what time it should run. After this tool returns, "
|
||||
"do not send another assistant message like 'Done'; the UI prompt "
|
||||
"will handle the next step."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
@@ -324,6 +341,7 @@ def handle_schedule_workflow(args: dict) -> dict:
|
||||
"steps": [{"id": f"s{i+1}", "text": s} for i, s in enumerate(steps_in) if s],
|
||||
"schedule": schedule,
|
||||
"source_session_id": args.get("source_session_id") or PARENT_SESSION_ID or None,
|
||||
"dashboard_id": DASHBOARD_ID or None,
|
||||
}
|
||||
r = _call("POST", "/create", body)
|
||||
if "_error" in r:
|
||||
|
||||
@@ -282,7 +282,16 @@ async def create_workflow(body: WorkflowCreate):
|
||||
pass
|
||||
storage.save_workflow(wf)
|
||||
scheduler.kick()
|
||||
return _enriched(wf)
|
||||
enriched = _enriched(wf)
|
||||
try:
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
await ws_manager.broadcast_global("workflow:updated", {
|
||||
"workflow_id": wf.id,
|
||||
"workflow": enriched,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return enriched
|
||||
|
||||
|
||||
async def _generate_workflow_metadata(wf: Workflow) -> tuple[str, str, list[str]]:
|
||||
|
||||
Reference in New Issue
Block a user