[aidan] feat/agent-scheduling: route recurring asks through native workflows, deny claude cron skill

This commit is contained in:
abccodes
2026-06-19 03:41:50 -07:00
parent b60aa522dc
commit 7608d0bc17
3 changed files with 104 additions and 26 deletions
+64 -13
View File
@@ -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
+30 -12
View File
@@ -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:
+10 -1
View File
@@ -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]]: