スケジューリングガイド¶
概要¶
Symbiontのスケジューリングシステムは、AIエージェント向けの本番レベルのcronベースタスク実行機能を提供します。以下の機能をサポートしています:
- cronスケジュール: 定期タスク用の標準的なcron構文
- ワンショットジョブ: 指定時刻に一度だけ実行
- ハートビートパターン: 監視エージェント向けの継続的な評価-アクション-スリープサイクル
- セッション分離: エフェメラル、共有、または完全分離のエージェントコンテキスト
- 配信ルーティング: 複数の出力チャネル(Stdout、LogFile、Webhook、Slack、Email、Custom)
- ポリシー適用: 実行前のセキュリティおよびコンプライアンスチェック
- 本番環境の堅牢化: ジッター、同時実行制限、デッドレターキュー、AgentPin検証
アーキテクチャ¶
スケジューリングシステムは3つのコアコンポーネントで構成されています:
┌─────────────────────┐
│ CronScheduler │ バックグラウンドティックループ(1秒間隔)
│ (Tick Loop) │ ジョブ選択と実行オーケストレーション
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ SqliteJobStore │ 永続的なジョブストレージ
│ (Job Storage) │ トランザクションサポート、状態管理
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│DefaultAgentScheduler│ エージェント実行ランタイム
│ (Execution Engine) │ AgentContextライフサイクル管理
└─────────────────────┘
CronScheduler¶
CronSchedulerは主要なエントリポイントです。以下を管理します:
- 1秒間隔で動作するバックグラウンドティックループ
- 次回実行時刻に基づくジョブ選択
- 同時実行制御とジッター挿入
- メトリクス収集とヘルスモニタリング
- 実行中ジョブの追跡を伴うグレースフルシャットダウン
SqliteJobStore¶
SqliteJobStoreは以下の機能を備えた永続的なジョブストレージを提供します:
- ジョブ状態更新のためのACIDトランザクション
- ジョブライフサイクル追跡(Active、Paused、Completed、Failed、DeadLetter)
- 監査証跡付きの実行履歴
- ステータス、エージェントIDなどによるフィルタリングクエリ機能
DefaultAgentScheduler¶
DefaultAgentSchedulerはスケジュールされたエージェントを実行します:
- 分離または共有の
AgentContextインスタンスを作成 - セッションライフサイクル(作成、実行、破棄)を管理
- 設定されたチャネルへの配信をルーティング
- 実行前にポリシーゲートを適用
DSL構文¶
スケジュールブロックの構造¶
スケジュールブロックはSymbiont DSLファイルで定義されます:
schedule {
name: "daily-report"
agent: "reporter-agent"
cron: "0 0 9 * * *"
session_mode: "ephemeral_with_summary"
delivery: ["stdout", "log_file"]
policy {
require_approval: false
max_runtime: "5m"
}
}
Cron構文¶
6つのフィールドを持つ拡張cron構文(秒が先頭、オプションの7番目のフィールドは年):
┌─────────────── 秒 (0-59)
│ ┌───────────── 分 (0-59)
│ │ ┌─────────── 時 (0-23)
│ │ │ ┌───────── 日 (1-31)
│ │ │ │ ┌─────── 月 (1-12)
│ │ │ │ │ ┌───── 曜日 (0-6, 日曜日 = 0)
│ │ │ │ │ │
* * * * * *
例:
# 毎日午前9時
cron: "0 0 9 * * *"
# 毎週月曜日の午後6時
cron: "0 0 18 * * 1"
# 15分ごと
cron: "0 */15 * * * *"
# 毎月1日の深夜0時
cron: "0 0 0 1 * *"
ワンショットジョブ(At構文)¶
指定時刻に一度だけ実行するジョブの場合:
schedule {
name: "deployment-check"
agent: "health-checker"
at: "2026-02-15T14:30:00Z" # ISO 8601タイムスタンプ
delivery: ["webhook"]
webhook_url: "https://ops.example.com/hooks/deployment"
}
ハートビートパターン¶
評価 → アクション → スリープの継続的な監視エージェント向け:
schedule {
name: "system-monitor"
agent: "heartbeat-agent"
cron: "0 */5 * * * *" # 5分ごとに起動
heartbeat: {
enabled: true
context_mode: "ephemeral_with_summary"
max_iterations: 100 # 安全制限
}
}
ハートビートエージェントは以下のサイクルに従います:
- 評価: システム状態を評価(例:メトリクス、ログの確認)
- アクション: 必要に応じて是正措置を実行(例:サービスの再起動、運用チームへのアラート)
- スリープ: 次のスケジュールされたティックまで待機
CLIコマンド¶
symbi cronコマンドにより、完全なライフサイクル管理が可能です:
ジョブ一覧¶
# すべてのジョブを一覧表示
symbi cron list
# ステータスでフィルタリング
symbi cron list --status active
symbi cron list --status paused
# エージェントでフィルタリング
symbi cron list --agent "reporter-agent"
# JSON出力
symbi cron list --format json
ジョブ追加¶
# DSLファイルから追加
symbi cron add --file agent.symbi --schedule "daily-report"
# インライン定義(JSON)
symbi cron add --json '{
"name": "quick-task",
"agent_id": "agent-123",
"cron_expr": "0 0 * * * *"
}'
ジョブ削除¶
# ジョブIDで削除
symbi cron remove <job-id>
# 名前で削除
symbi cron remove --name "daily-report"
# 強制削除(確認をスキップ)
symbi cron remove <job-id> --force
一時停止/再開¶
ステータス¶
# 次回実行時刻を含むジョブ詳細
symbi cron status <job-id>
# 直近10件の実行記録を含む
symbi cron status <job-id> --history 10
# ウォッチモード(5秒ごとに自動更新)
symbi cron status <job-id> --watch
即時実行¶
# 即時実行をトリガー(スケジュールをバイパス)
symbi cron run <job-id>
# カスタム入力付きで実行
symbi cron run <job-id> --input "Check production database"
履歴¶
# ジョブの実行履歴を表示
symbi cron history <job-id>
# 直近20件の実行
symbi cron history <job-id> --limit 20
# ステータスでフィルタリング
symbi cron history <job-id> --status failed
# CSVにエクスポート
symbi cron history <job-id> --format csv > runs.csv
ハートビートパターン¶
HeartbeatContextMode¶
ハートビートのイテレーション間でコンテキストがどのように保持されるかを制御します:
pub enum HeartbeatContextMode {
/// Fresh context each iteration, append summary to run history
EphemeralWithSummary,
/// Shared context across all iterations (memory accumulates)
SharedPersistent,
/// Fresh context each iteration, no summary (stateless)
FullyEphemeral,
}
EphemeralWithSummary(デフォルト):
- イテレーションごとに新しいAgentContextを作成
- 前回のイテレーションのサマリーをコンテキストに追加
- 無制限のメモリ増加を防止
- 関連するアクション間の継続性を維持
SharedPersistent:
- すべてのイテレーションで単一のAgentContextを再利用
- 完全な会話履歴を保持
- メモリ使用量が高い
- 深いコンテキストを必要とするエージェントに最適(例:デバッグセッション)
FullyEphemeral:
- イテレーションごとに新しいAgentContext、引き継ぎなし
- 最小のメモリフットプリント
- 独立したチェックに最適(例:APIヘルスプローブ)
ハートビートエージェントの例¶
agent heartbeat_monitor {
model: "claude-sonnet-4.5"
system_prompt: """
You are a system monitoring agent. On each heartbeat:
1. Check system metrics (CPU, memory, disk)
2. Review recent error logs
3. If issues detected, take action:
- Restart services if safe
- Alert ops team via Slack
- Log incident details
4. Summarize findings
5. Return 'sleep' when done
"""
}
schedule {
name: "heartbeat-monitor"
agent: "heartbeat_monitor"
cron: "0 */10 * * * *" # 10分ごと
heartbeat: {
enabled: true
context_mode: "ephemeral_with_summary"
max_iterations: 50
}
delivery: ["log_file", "slack"]
slack_channel: "#ops-alerts"
}
セッション分離¶
セッションモード¶
pub enum HeartbeatContextMode {
/// Ephemeral context with summary carryover (default)
EphemeralWithSummary,
/// Shared persistent context across all runs
SharedPersistent,
/// Fully ephemeral, no state carryover
FullyEphemeral,
}
設定:
schedule {
name: "data-pipeline"
agent: "etl-agent"
cron: "0 0 2 * * *"
# 実行ごとに新しいコンテキスト、前回実行のサマリーを含む
session_mode: "ephemeral_with_summary"
}
セッションライフサイクル¶
スケジュールされた各実行について:
- 実行前: 同時実行制限の確認、ジッターの適用
- セッション作成:
session_modeに基づいてAgentContextを作成 - ポリシーゲート: ポリシー条件を評価
- 実行: 入力とコンテキストでエージェントを実行
- 配信: 設定されたチャネルに出力をルーティング
- セッションクリーンアップ: モードに基づいてコンテキストを破棄または保持
- 実行後: 実行記録の更新、メトリクスの収集
配信ルーティング¶
サポートされるチャネル¶
pub enum DeliveryChannel {
Stdout, // Print to console
LogFile, // Append to job-specific log file
Webhook, // HTTP POST to URL
Slack, // Slack webhook or API
Email, // SMTP email
Custom(String), // User-defined channel
}
設定例¶
単一チャネル:
複数チャネル:
schedule {
name: "security-scan"
agent: "scanner"
cron: "0 0 1 * * *"
delivery: ["log_file", "slack", "email"]
slack_channel: "#security"
email_recipients: ["ops@example.com", "security@example.com"]
}
Webhook配信:
schedule {
name: "metrics-report"
agent: "metrics-agent"
cron: "0 */30 * * * *"
delivery: ["webhook"]
webhook_url: "https://metrics.example.com/ingest"
webhook_headers: {
"Authorization": "Bearer ${METRICS_API_KEY}"
"Content-Type": "application/json"
}
}
DeliveryRouterトレイト¶
カスタム配信チャネルは以下を実装します:
#[async_trait]
pub trait DeliveryRouter: Send + Sync {
async fn route(
&self,
channel: &DeliveryChannel,
job: &CronJobDefinition,
run: &JobRunRecord,
output: &str,
) -> Result<(), SchedulerError>;
}
ポリシー適用¶
PolicyGate¶
PolicyGateは実行前にスケジュール固有のポリシーを評価します:
pub struct PolicyGate {
policy_engine: Arc<RealPolicyParser>,
}
impl PolicyGate {
pub fn evaluate(
&self,
job: &CronJobDefinition,
context: &AgentContext,
) -> Result<SchedulePolicyDecision, SchedulerError>;
}
ポリシー条件¶
schedule {
name: "production-deploy"
agent: "deploy-agent"
cron: "0 0 0 * * 0" # 日曜深夜
policy {
# 実行前に人間の承認を要求
require_approval: true
# 強制終了までの最大実行時間
max_runtime: "30m"
# 特定のケイパビリティを要求
require_capabilities: ["deployment", "production_write"]
# 時間枠の適用(UTC)
allowed_hours: {
start: "00:00"
end: "04:00"
}
# 環境制限
allowed_environments: ["staging", "production"]
# AgentPin検証を要求
require_agent_pin: true
}
}
SchedulePolicyDecision¶
pub enum SchedulePolicyDecision {
Allow,
Deny { reason: String },
RequiresApproval { approver: String, reason: String, policy_id: String },
}
本番環境の堅牢化¶
ジッター¶
複数のジョブが同じスケジュールを共有する場合のサンダリングハード問題を防止します:
例:
ジョブごとの同時実行制限¶
リソース枯渇を防ぐためにジョブごとの同時実行数を制限します:
schedule {
name: "data-sync"
agent: "sync-agent"
cron: "0 */5 * * * *"
max_concurrent: 2 # 最大2つの同時実行を許可
}
ジョブが最大同時実行数で既に実行中の場合、スケジューラーはそのティックをスキップします。
デッドレターキュー¶
max_retriesを超えたジョブは手動レビューのためにDeadLetterステータスに移行します:
schedule {
name: "flaky-job"
agent: "unreliable-agent"
cron: "0 0 * * * *"
max_retries: 3 # 3回失敗後、デッドレターに移動
}
復旧:
# デッドレター化されたジョブを一覧表示
symbi cron list --status dead_letter
# 失敗理由を確認
symbi cron history <job-id> --status failed
# 修正後にジョブをアクティブにリセット
symbi cron reset <job-id>
AgentPin検証¶
実行前にエージェントのIDを暗号的に検証します:
schedule {
name: "secure-task"
agent: "trusted-agent"
cron: "0 0 * * * *"
agent_pin_jwt: "${AGENT_PIN_JWT}" # agentpin-cliからのES256 JWT
policy {
require_agent_pin: true
}
}
スケジューラーは以下を検証します:
1. ES256(ECDSA P-256)を使用したJWT署名
2. エージェントIDがissクレームと一致
3. ドメインアンカーが期待されるオリジンと一致
4. 有効期限(exp)が有効
検証失敗時はSecurityEventType::AgentPinVerificationFailed監査イベントが発行されます。
HTTP APIエンドポイント¶
スケジュール管理¶
POST /api/v1/schedule 新しいスケジュールジョブを作成します。
curl -X POST http://localhost:8080/api/v1/schedule \
-H "Content-Type: application/json" \
-d '{
"name": "hourly-report",
"agent_id": "reporter",
"cron_expr": "0 0 * * * *",
"session_mode": "ephemeral_with_summary",
"delivery": ["stdout"]
}'
GET /api/v1/schedule すべてのジョブを一覧表示(ステータス、エージェントIDでフィルタリング可能)。
GET /api/v1/schedule/{job_id} ジョブの詳細を取得します。
PUT /api/v1/schedule/{job_id} ジョブを更新(cron式、配信先など)。
curl -X PUT http://localhost:8080/api/v1/schedule/job-123 \
-H "Content-Type: application/json" \
-d '{"cron_expr": "0 0 */2 * * *"}'
DELETE /api/v1/schedule/{job_id} ジョブを削除します。
POST /api/v1/schedule/{job_id}/pause ジョブを一時停止します。
POST /api/v1/schedule/{job_id}/resume 一時停止中のジョブを再開します。
POST /api/v1/schedule/{job_id}/run 即時実行をトリガーします。
curl -X POST http://localhost:8080/api/v1/schedule/job-123/run \
-H "Content-Type: application/json" \
-d '{"input": "Run with custom input"}'
GET /api/v1/schedule/{job_id}/history 実行履歴を取得します。
GET /api/v1/schedule/{job_id}/next_run 次回スケジュール実行時刻を取得します。
ヘルスモニタリング¶
GET /api/v1/health/scheduler スケジューラーのヘルスとメトリクス。
レスポンス:
{
"status": "healthy",
"active_jobs": 15,
"paused_jobs": 3,
"in_flight_jobs": 2,
"metrics": {
"runs_total": 1234,
"runs_succeeded": 1180,
"runs_failed": 54,
"avg_execution_time_ms": 850
}
}
SDKの例¶
JavaScript SDK¶
import { SymbiontClient } from '@symbiont/sdk-js';
const client = new SymbiontClient({
baseUrl: 'http://localhost:8080',
apiKey: process.env.SYMBI_API_KEY
});
// スケジュールジョブを作成
const job = await client.schedule.create({
name: 'daily-backup',
agentId: 'backup-agent',
cronExpr: '0 0 2 * * *',
sessionMode: 'ephemeral_with_summary',
delivery: ['webhook'],
webhookUrl: 'https://backup.example.com/notify'
});
console.log(`Created job: ${job.id}`);
// アクティブなジョブを一覧表示
const activeJobs = await client.schedule.list({ status: 'active' });
console.log(`Active jobs: ${activeJobs.length}`);
// ジョブのステータスを取得
const status = await client.schedule.getStatus(job.id);
console.log(`Next run: ${status.next_run}`);
// 即時実行をトリガー
await client.schedule.runNow(job.id, { input: 'Backup database' });
// ジョブを一時停止
await client.schedule.pause(job.id);
// 履歴を表示
const history = await client.schedule.getHistory(job.id, { limit: 10 });
history.forEach(run => {
console.log(`Run ${run.id}: ${run.status} (${run.execution_time_ms}ms)`);
});
// ジョブを再開
await client.schedule.resume(job.id);
// ジョブを削除
await client.schedule.delete(job.id);
Python SDK¶
from symbiont import SymbiontClient
client = SymbiontClient(
base_url='http://localhost:8080',
api_key=os.environ['SYMBI_API_KEY']
)
# スケジュールジョブを作成
job = client.schedule.create(
name='hourly-metrics',
agent_id='metrics-agent',
cron_expr='0 0 * * * *',
session_mode='ephemeral_with_summary',
delivery=['slack', 'log_file'],
slack_channel='#metrics'
)
print(f"Created job: {job.id}")
# 特定のエージェントのジョブを一覧表示
jobs = client.schedule.list(agent_id='metrics-agent')
print(f"Found {len(jobs)} jobs for metrics-agent")
# ジョブの詳細を取得
details = client.schedule.get(job.id)
print(f"Cron: {details.cron_expr}")
print(f"Next run: {details.next_run}")
# cron式を更新
client.schedule.update(job.id, cron_expr='0 */30 * * * *')
# 即時実行をトリガー
run = client.schedule.run_now(job.id, input='Generate metrics report')
print(f"Run ID: {run.id}")
# メンテナンス中に一時停止
client.schedule.pause(job.id)
print("Job paused for maintenance")
# 最近の失敗を表示
history = client.schedule.get_history(
job.id,
status='failed',
limit=5
)
for run in history:
print(f"Failed run {run.id}: {run.error_message}")
# メンテナンス後に再開
client.schedule.resume(job.id)
# スケジューラーのヘルスを確認
health = client.schedule.health()
print(f"Scheduler status: {health.status}")
print(f"Active jobs: {health.active_jobs}")
print(f"In-flight jobs: {health.in_flight_jobs}")
設定¶
CronSchedulerConfig¶
pub struct CronSchedulerConfig {
/// Tick interval (default: 1 second)
pub tick_interval: Duration,
/// Global concurrency limit (default: 100)
pub max_concurrent_cron_jobs: usize,
/// Persistent job store path (default: None)
pub job_store_path: Option<PathBuf>,
/// Catch up missed runs on startup (default: true)
pub enable_missed_run_catchup: bool,
}
TOML設定¶
[scheduler]
tick_interval_seconds = 1
max_jitter_seconds = 30
max_concurrent_jobs = 20
enable_metrics = true
default_max_retries = 3
shutdown_timeout_seconds = 60
[scheduler.delivery]
# Webhook設定
webhook_timeout_seconds = 30
webhook_retry_attempts = 3
# Slack設定
slack_api_token = "${SLACK_API_TOKEN}"
slack_default_channel = "#ops"
# メール設定
smtp_host = "smtp.example.com"
smtp_port = 587
smtp_username = "${SMTP_USER}"
smtp_password = "${SMTP_PASS}"
email_from = "symbiont@example.com"
環境変数¶
# スケジューラー設定
SYMBI_SCHEDULER_MAX_JITTER=30
SYMBI_SCHEDULER_MAX_CONCURRENT=20
# 配信設定
SYMBI_SLACK_TOKEN=xoxb-...
SYMBI_WEBHOOK_AUTH_HEADER="Bearer secret-token"
# AgentPin検証
SYMBI_AGENTPIN_REQUIRED=true
SYMBI_AGENTPIN_DOMAIN=agent.example.com
可観測性¶
メトリクス(Prometheus互換)¶
# 合計実行数
symbiont_cron_runs_total{job_name="daily-report",status="succeeded"} 450
# 失敗した実行
symbiont_cron_runs_total{job_name="daily-report",status="failed"} 5
# 実行時間ヒストグラム
symbiont_cron_execution_duration_seconds{job_name="daily-report"} 1.234
# 実行中ジョブゲージ
symbiont_cron_in_flight_jobs 3
# デッドレター化されたジョブ
symbiont_cron_dead_letter_total{job_name="flaky-job"} 2
監査イベント¶
すべてのスケジューラーアクションはセキュリティイベントを発行します:
pub enum SecurityEventType {
CronJobCreated,
CronJobUpdated,
CronJobDeleted,
CronJobPaused,
CronJobResumed,
CronJobExecuted,
CronJobFailed,
CronJobDeadLettered,
AgentPinVerificationFailed,
}
監査ログのクエリ:
ベストプラクティス¶
- 共有スケジュールにはジッターを使用: 複数のジョブが同時に開始するのを防止
- 同時実行制限を設定: リソース枯渇から保護
- デッドレターキューを監視: 失敗しているジョブを定期的にレビューして修正
- EphemeralWithSummaryを使用: 長時間実行されるハートビートでの無制限のメモリ増加を防止
- AgentPin検証を有効化: エージェントのIDを暗号的に検証
- 配信ルーティングを設定: ジョブタイプに応じた適切なチャネルを使用
- ポリシーゲートを設定: 時間枠、承認、ケイパビリティチェックを適用
- 監視にはハートビートパターンを使用: 継続的な評価-アクション-スリープサイクル
- ステージングでスケジュールをテスト: 本番環境前にcron式とジョブロジックを検証
- メトリクスをエクスポート: 運用の可視化のためにPrometheus/Grafanaと統合
トラブルシューティング¶
ジョブが実行されない¶
- ジョブのステータスを確認:
symbi cron status <job-id> - cron式を検証: crontab.guruを使用
- スケジューラーのヘルスを確認:
curl http://localhost:8080/api/v1/health/scheduler - ログを確認:
symbi logs --filter scheduler --level debug
ジョブが繰り返し失敗する¶
- 履歴を表示:
symbi cron history <job-id> --status failed - 実行記録のエラーメッセージを確認
- エージェントの設定とケイパビリティを検証
- スケジューラー外でエージェントをテスト:
symbi run <agent-id> --input "test" - ポリシーゲートを確認: 時間枠とケイパビリティが一致しているか確認
デッドレター化されたジョブ¶
- デッドレタージョブを一覧表示:
symbi cron list --status dead_letter - 失敗パターンを確認:
symbi cron history <job-id> - 根本原因を修正(エージェントコード、権限、外部依存関係)
- ジョブをリセット:
symbi cron reset <job-id>
高メモリ使用量¶
- セッションモードを確認:
ephemeral_with_summaryまたはfully_ephemeralに切り替え - ハートビートのイテレーションを削減:
max_iterationsを低く設定 - コンテキストサイズを監視: エージェントの出力の冗長性を確認
- コンテキストのアーカイブを有効化: 保持ポリシーを設定
v0.9.0からの移行¶
v1.0.0リリースでは本番環境の堅牢化機能が追加されました。ジョブ定義を更新してください:
schedule {
name: "my-job"
agent: "my-agent"
cron: "0 0 * * * *"
+
+ # 同時実行制限を追加
+ max_concurrent: 2
+
+ # ID検証のためのAgentPinを追加
+ agent_pin_jwt: "${AGENT_PIN_JWT}"
+
+ policy {
+ require_agent_pin: true
+ }
}
設定を更新:
[scheduler]
tick_interval_seconds = 1
+ max_jitter_seconds = 30
+ default_max_retries = 3
+ shutdown_timeout_seconds = 60
破壊的なAPI変更はありません。すべてのv0.9.0ジョブは引き続き動作します。