-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(routines): approval context for autonomous job execution #577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0a1195d
910b930
f5760e7
2bc6c0e
d1e0594
8eafa57
d0c7c84
55d4acb
850b9df
35847fe
c138926
70e0e9d
0ee09bd
81c3c2d
fc15ff0
1a38775
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,7 @@ use crate::config::RoutineConfig; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::db::Database; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::error::RoutineError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::llm::{ChatMessage, CompletionRequest, FinishReason, LlmProvider}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::tools::ApprovalContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::workspace::Workspace; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The routine execution engine. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -180,6 +181,9 @@ impl RoutineEngine { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fire a routine manually (from tool call or CLI). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Bypasses cooldown checks (those only apply to cron/event triggers). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Still enforces enabled check and concurrent run limit. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub async fn fire_manual(&self, routine_id: Uuid) -> Result<Uuid, RoutineError> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let routine = self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .store | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -327,7 +331,19 @@ async fn execute_routine(ctx: EngineContext, routine: Routine, run: RoutineRun) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_iterations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } => execute_full_job(&ctx, &routine, &run, title, description, *max_iterations).await, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_permissions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execute_full_job( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &ctx, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &routine, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &run, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *max_iterations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_permissions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Decrement running count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -418,6 +434,7 @@ async fn execute_full_job( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: &str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: &str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_iterations: u32, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_permissions: &[String], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> Result<(RunStatus, Option<String>, Option<i32>), RoutineError> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let scheduler = ctx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .scheduler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -426,10 +443,31 @@ async fn execute_full_job( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: "scheduler not available".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set the message tool's default channel/target from the routine's notify config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so the LLM can send results without triggering cross-channel approval. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: This mutates shared global state and can race with concurrent jobs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Move notify config into JobContext metadata and apply per-job instead. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(channel) = &routine.notify.channel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scheduler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .tools() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .set_message_tool_context(Some(channel.clone()), Some(routine.notify.user.clone())) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .await; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let metadata = serde_json::json!({ "max_iterations": max_iterations }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+446
to
458
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set the message tool's default channel/target from the routine's notify config | |
| // so the LLM can send results without triggering cross-channel approval. | |
| // TODO: This mutates shared global state and can race with concurrent jobs. | |
| // Move notify config into JobContext metadata and apply per-job instead. | |
| if let Some(channel) = &routine.notify.channel { | |
| scheduler | |
| .tools() | |
| .set_message_tool_context(Some(channel.clone()), Some(routine.notify.user.clone())) | |
| .await; | |
| } | |
| let metadata = serde_json::json!({ "max_iterations": max_iterations }); | |
| // Embed the routine's notify config into the job metadata so that any | |
| // message tools can derive channel/target per job instead of mutating | |
| // shared global state. | |
| let metadata = serde_json::json!({ | |
| "max_iterations": max_iterations, | |
| "notify": { | |
| "channel": routine.notify.channel, | |
| "user": routine.notify.user, | |
| }, | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate of the earlier comment on the same line. TODO already added in 55d4acb noting the race. Will move to per-job metadata in a follow-up.
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This updates shared/global message tool defaults, which can race when multiple jobs/routines run concurrently (e.g., one routine can accidentally change another routine’s default channel/target, impacting both routing and approval behavior). Prefer setting per-job message context (e.g., store notify channel/user in JobContext/metadata and have MessageTool resolve defaults from the executing job context rather than global mutable state).
| // Set the message tool's default channel/target from the routine's notify config | |
| // so the LLM can send results without triggering cross-channel approval. | |
| // TODO: This mutates shared global state and can race with concurrent jobs. | |
| // Move notify config into JobContext metadata and apply per-job instead. | |
| if let Some(channel) = &routine.notify.channel { | |
| scheduler | |
| .tools() | |
| .set_message_tool_context(Some(channel.clone()), Some(routine.notify.user.clone())) | |
| .await; | |
| } | |
| let metadata = serde_json::json!({ "max_iterations": max_iterations }); | |
| // Build approval context: UnlessAutoApproved tools are auto-approved for routines; | |
| // Always tools require explicit listing in tool_permissions. | |
| let approval_context = ApprovalContext::autonomous_with_tools(tool_permissions.iter().cloned()); | |
| // Attach notify configuration to the job metadata so the worker/message tool | |
| // can resolve per-job defaults instead of mutating shared global state. | |
| let metadata = serde_json::json!({ | |
| "max_iterations": max_iterations, | |
| "notify": { | |
| "channel": routine.notify.channel, | |
| "user": routine.notify.user, | |
| }, | |
| }); | |
| // Build approval context: UnlessAutoApproved tools are auto-approved for routines; | |
| // Always tools require explicit listing in tool_permissions. | |
| let approval_context = ApprovalContext::autonomous_with_tools(tool_permissions.iter().cloned()); | |
| let approval_context = ApprovalContext::autonomous_with_tools(tool_permissions.iter().cloned()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate of earlier comment. TODO already added in 55d4acb. Will move to per-job metadata in a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting the message tool’s default channel/target via ToolRegistry mutates shared global state. Because the scheduler can run multiple jobs concurrently, this can race with other jobs/interactive turns and cause messages to be sent to the wrong channel/target when the message tool is invoked without explicit params (and it also affects requires_approval() decisions). Prefer storing the routine’s notify channel/target in the JobContext (e.g., metadata) and applying it per-job immediately before message tool approval/execute, or require routines to always pass explicit channel/target instead of relying on global defaults.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid concern. Added TODO comment in 55d4acb noting the race. Will move notify config into JobContext metadata in a follow-up.