/// When a routine should fire.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Trigger {
/// Fire on a cron schedule (e.g. "0 9 * * MON-FRI" or "every 2h").
Cron {
schedule: String,
#[serde(default)]
timezone: Option<String>,
},
/// Fire when a channel message matches a pattern.
Event {
/// Optional channel filter (e.g. "telegram", "slack").
channel: Option<String>,
/// Regex pattern to match against message content.
pattern: String,
},
/// Fire when a structured system event is emitted.
SystemEvent {
/// Event source namespace (e.g. "github", "workflow", "tool").
source: String,
/// Event type within the source (e.g. "issue.opened").
event_type: String,
/// Optional exact-match filters against payload top-level fields.
#[serde(default)]
filters: std::collections::HashMap<String, String>,
},
/// Fire on incoming webhook POST to /api/webhooks/{path}.
Webhook {
/// Optional webhook path suffix (defaults to routine id).
path: Option<String>,
/// Optional shared secret for HMAC validation.
secret: Option<String>,
},
/// Only fires via tool call or CLI.
Manual,
}
/// What happens when a routine fires.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RoutineAction {
/// Single LLM call (optionally with tools). Cheap and fast.
Lightweight {
/// The prompt sent to the LLM.
prompt: String,
/// Workspace paths to load as context (e.g. ["context/priorities.md"]).
#[serde(default)]
context_paths: Vec<String>,
/// Max output tokens (default: 4096).
#[serde(default = "default_max_tokens")]
max_tokens: u32,
/// Enable tool access (default: false for backward compatibility).
/// When true, the LLM can call tools during execution.
/// Tools requiring approval are automatically filtered out.
#[serde(default)]
use_tools: bool,
/// Max tool call rounds (default: 3). Only used when use_tools is true.
#[serde(default = "default_max_tool_rounds")]
max_tool_rounds: u32,
},
/// Full multi-turn worker job with tool access.
FullJob {
/// Job title for the scheduler.
title: String,
/// Job description / initial prompt.
description: String,
/// Max reasoning iterations (default: 10).
#[serde(default = "default_max_iterations")]
max_iterations: u32,
},
}
┌───────────┬──────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 维度 │ RoutineAction::Lightweight │ RoutineAction::FullJob │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 执行实体 │ RoutineEngine 内部 inline │ 委托给 Scheduler │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 跑在哪 │ 引擎 spawn 的轻量 task,跟 routine run 同一 task │ 独立 worker job(在 jobs map 里占一个 slot,受 max_parallel_jobs 限制) │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ LLM 调用 │ 单次 llm.chat(可选用工具走 N 轮小循环) │ 走完整 agentic loop,多轮推理(默认 25 次 max_iterations,RoutineEngine 字段定义为 10 但实际默认 │
│ │ │ 25,routine.rs:330) │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 工具 │ engine.tools(lightweight 集),默认关(use_tools: false),开启时需走 │ 完整 ToolDispatcher::dispatch,包含 ActionRecord 审计、参数脱敏、超时、输出脱敏 │
│ │ autonomous_allowed_tool_names 过滤(带"approval"标记的工具自动剔除) │ │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 输出上限 │ max_tokens(默认 4096) │ max_iterations(默认 25)—— 整轮推理循环上限 │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 会话/线程 │ 临时会话,无独立 thread │ 创建独立 JobContext、有独立 UserId、在 ContextManager 里持久化 │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 通知 │ 通过 engine.notify_tx mpsc 直接 send OutgoingResponse 到 ChannelManager │ job 完成后经 JobMonitor/SSE 灌回主 loop │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 沙箱 │ 不需要(轻量,本机直跑) │ 看 sandbox_readiness:Docker 不可用 → 拒绝派发(fail-closed);DisabledByConfig → 退化为 host │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 完成监控 │ 直接 await,结果写到 run.result_summary │ FullJobWatcher::wait_for_completion(routine_engine.rs:1538),5s 间隔轮询 DB,routine run 必须 │
│ │ │ link 到 job_id(不 link 永久卡 "running" 状态) │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DB 持久化 │ 仅写 routine_runs 表 │ 写 routine_runs + 独立的 jobs 行 + job_actions / llm_calls(FK 引用) │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 重试语义 │ 失败重试有 token 累计保留 │ 失败有专门的 JobState::Stuck → SelfRepair 路径 │
├───────────┼──────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 适用场景 │ "每 N 分钟看一眼 HEARTBEAT.md,通知我" "扫一下邮箱标题,挑重要的" │ "每天早上跑一个完整调研、生成报告、写到 Notion" │
└───────────┴──────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────┘