feat(cron-recipes): /cron-recipe <name> seeds a conversational fill

Reworks the chat-line UX: pick a recipe by name and the agent asks you for
what it needs, one question at a time, instead of forcing you to hand-type a
slot=val command line.

- /cron-recipe                  -> lists the catalog
- /cron-recipe <name>           -> forgiving name match (exact/prefix/substring/
                                   fuzzy; ambiguous lists candidates), then seeds
                                   the agent with a natural-language fill request
                                   built from the recipe's typed slots + schedule
                                   and prompt templates. The agent asks for each
                                   value one at a time and calls the EXISTING
                                   cronjob tool. No new tool.
- /cron-recipe <name> slot=val  -> unchanged deterministic path (fill_recipe ->
                                   create_job) for the dashboard/docs/power user.

Mechanism (no new plumbing, invariant-safe — the seed enters as a normal user
turn, never a synthetic injection):
- shared handler returns RecipeCommandResult{text, agent_seed}; match_recipe()
  and build_recipe_seed() are the new shared pieces.
- gateway: dispatch rewrites event.text to the seed and falls through to the
  agent (the same pattern /steer uses).
- CLI: handler sets a one-shot self._pending_agent_seed; the interactive loop
  consumes it right after process_command() and runs it as the next turn.

The typed-slot schema stays the single source of truth (still validates the
form/inline path via fill_recipe); the agent path just renders those slots into
the questions to ask. Docs updated to lead with the name-then-ask flow.
This commit is contained in:
teknium1
2026-06-11 10:49:47 -07:00
committed by Teknium
parent 1593ca5406
commit e976faac7a
7 changed files with 299 additions and 75 deletions
+23 -5
View File
@@ -7175,7 +7175,20 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return await self._handle_suggestions_command(event)
if canonical == "cron-recipe":
return await self._handle_cron_recipe_command(event)
_recipe_result = await self._handle_cron_recipe_command(event)
_recipe_seed = getattr(_recipe_result, "agent_seed", None)
if _recipe_seed:
# Recipe matched — rewrite the turn to the seed and fall
# through to _handle_message_with_agent so the agent asks the
# user for each slot value conversationally and then calls the
# cronjob tool (the /steer fall-through pattern). The seed
# enters as a normal user turn, preserving role alternation.
try:
event.text = _recipe_seed
except Exception:
return getattr(_recipe_result, "text", "") or None
else:
return getattr(_recipe_result, "text", "") or None
if canonical == "retry":
return await self._handle_retry_command(event)
@@ -9273,12 +9286,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
logger.debug("suggestions command failed: %s", e)
return f"Suggestions command failed: {e}"
async def _handle_cron_recipe_command(self, event: MessageEvent) -> str:
async def _handle_cron_recipe_command(self, event: MessageEvent):
"""Handle /cron-recipe in the gateway.
Delegates to the shared handler so CLI, TUI, and gateway never drift.
Origin is built from the event source so a created recipe job delivers
back to this chat/thread.
Returns a RecipeCommandResult: ``text`` is shown to the user, and if
``agent_seed`` is set the dispatch site rewrites ``event.text`` to the
seed and falls through to the agent (the ``/steer`` pattern) so the
agent gathers the slot values conversationally. Origin is built from the
event source so a directly created recipe job delivers back to this chat.
"""
args = (event.get_command_args() or "").strip()
source = event.source
@@ -9301,7 +9317,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return handle_cron_recipe_command(args, origin=origin)
except Exception as e:
logger.debug("cron-recipe command failed: %s", e)
return f"Cron recipe command failed: {e}"
from hermes_cli.cron_recipe_cmd import RecipeCommandResult
return RecipeCommandResult(f"Cron recipe command failed: {e}")
# ────────────────────────────────────────────────────────────────
# /goal — persistent cross-turn goals (Ralph-style loop)