mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Default new lightweight routines to tools-enabled (#1573)
* Default new lightweight routines to tools-enabled * Fix fmt and clippy on lightweight routine PR * Use grouped execution field in routine no-tools fixture * Align CLI routine defaults with tools-enabled lightweight mode
This commit is contained in:
@@ -1440,6 +1440,7 @@ fn handle_text_response(
|
|||||||
/// This is a simplified version of the full dispatcher loop:
|
/// This is a simplified version of the full dispatcher loop:
|
||||||
/// - Max 3-5 iterations (configurable)
|
/// - Max 3-5 iterations (configurable)
|
||||||
/// - Sequential tool execution (not parallel)
|
/// - Sequential tool execution (not parallel)
|
||||||
|
/// - Uses the owner's live autonomous tool scope when lightweight tools are enabled
|
||||||
/// - Auto-approval of non-Always tools
|
/// - Auto-approval of non-Always tools
|
||||||
/// - No hooks or approval dialogs
|
/// - No hooks or approval dialogs
|
||||||
async fn execute_lightweight_with_tools(
|
async fn execute_lightweight_with_tools(
|
||||||
|
|||||||
+47
-2
@@ -340,8 +340,8 @@ async fn create(
|
|||||||
prompt: prompt.to_string(),
|
prompt: prompt.to_string(),
|
||||||
context_paths: Vec::new(),
|
context_paths: Vec::new(),
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
use_tools: false,
|
use_tools: true,
|
||||||
max_tool_rounds: 0,
|
max_tool_rounds: 3,
|
||||||
},
|
},
|
||||||
guardrails: RoutineGuardrails {
|
guardrails: RoutineGuardrails {
|
||||||
cooldown: std::time::Duration::from_secs(cooldown_secs),
|
cooldown: std::time::Duration::from_secs(cooldown_secs),
|
||||||
@@ -685,6 +685,7 @@ fn truncate(s: &str, max_chars: usize) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::agent::routine::RoutineAction;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_relative_future() {
|
fn format_relative_future() {
|
||||||
@@ -743,4 +744,48 @@ mod tests {
|
|||||||
assert!(notify.on_failure); // safety: test-only assertion
|
assert!(notify.on_failure); // safety: test-only assertion
|
||||||
assert!(!notify.on_success); // safety: test-only assertion
|
assert!(!notify.on_success); // safety: test-only assertion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libsql")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cli_create_defaults_lightweight_routines_to_tools_enabled() {
|
||||||
|
let harness = crate::testing::TestHarnessBuilder::new().build().await;
|
||||||
|
let db = harness.db.clone();
|
||||||
|
|
||||||
|
run_routines_command(
|
||||||
|
RoutinesCommand::Create {
|
||||||
|
name: "cli-digest".to_string(),
|
||||||
|
schedule: "0 0 9 * * *".to_string(),
|
||||||
|
prompt: "Prepare the morning digest.".to_string(),
|
||||||
|
description: "CLI created routine".to_string(),
|
||||||
|
timezone: Some("UTC".to_string()),
|
||||||
|
cooldown: 300,
|
||||||
|
notify_channel: None,
|
||||||
|
},
|
||||||
|
db.clone(),
|
||||||
|
"user1",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("create routine");
|
||||||
|
|
||||||
|
let routine = db
|
||||||
|
.get_routine_by_name("user1", "cli-digest")
|
||||||
|
.await
|
||||||
|
.expect("get routine by name")
|
||||||
|
.expect("cli-digest should exist");
|
||||||
|
|
||||||
|
match routine.action {
|
||||||
|
RoutineAction::Lightweight {
|
||||||
|
use_tools,
|
||||||
|
max_tool_rounds,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert!(
|
||||||
|
use_tools,
|
||||||
|
"CLI-created lightweight routines should default to tools"
|
||||||
|
);
|
||||||
|
assert_eq!(max_tool_rounds, 3);
|
||||||
|
}
|
||||||
|
other => panic!("expected lightweight action, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ fn execution_properties() -> Value {
|
|||||||
},
|
},
|
||||||
"use_tools": {
|
"use_tools": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Only applies to lightweight mode. When true, safe non-approval tools are available."
|
"default": true,
|
||||||
|
"description": "Only applies to lightweight mode. New lightweight routines default this to true; when enabled, the routine can use the owner's live autonomous tool scope."
|
||||||
},
|
},
|
||||||
"max_tool_rounds": {
|
"max_tool_rounds": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -290,7 +291,7 @@ fn routine_request_discovery_schema() -> Value {
|
|||||||
fn lightweight_execution_variant() -> Value {
|
fn lightweight_execution_variant() -> Value {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Default lightweight execution. Applies when execution is omitted or execution.mode='lightweight'.",
|
"description": "Default lightweight execution. Applies when execution is omitted or execution.mode='lightweight'. New lightweight routines default to tools enabled unless execution.use_tools=false is set.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"mode": {
|
"mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -304,7 +305,8 @@ fn lightweight_execution_variant() -> Value {
|
|||||||
},
|
},
|
||||||
"use_tools": {
|
"use_tools": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "When true, safe non-approval tools are available."
|
"default": true,
|
||||||
|
"description": "Defaults to true for new lightweight routines. When enabled, the routine can use the owner's live autonomous tool scope."
|
||||||
},
|
},
|
||||||
"max_tool_rounds": {
|
"max_tool_rounds": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -335,7 +337,7 @@ fn full_job_execution_variant() -> Value {
|
|||||||
fn execution_discovery_schema() -> Value {
|
fn execution_discovery_schema() -> Value {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Optional execution settings. Omit this block for the default lightweight mode.",
|
"description": "Optional execution settings. Omit this block for the default lightweight mode with tools enabled.",
|
||||||
"properties": execution_properties(),
|
"properties": execution_properties(),
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
lightweight_execution_variant(),
|
lightweight_execution_variant(),
|
||||||
@@ -408,7 +410,8 @@ fn routine_create_tool_summary() -> ToolDiscoverySummary {
|
|||||||
"execution.mode='full_job' uses the owner's live autonomous tool scope and ignores use_tools, max_tool_rounds, and context_paths.".into(),
|
"execution.mode='full_job' uses the owner's live autonomous tool scope and ignores use_tools, max_tool_rounds, and context_paths.".into(),
|
||||||
],
|
],
|
||||||
notes: vec![
|
notes: vec![
|
||||||
"Omitting execution defaults to lightweight mode.".into(),
|
"Omitting execution defaults to lightweight mode with tools enabled.".into(),
|
||||||
|
"Set execution.use_tools=false to keep a new lightweight routine text-only.".into(),
|
||||||
"Omitting delivery.user falls back to the owner's last-seen notification target.".into(),
|
"Omitting delivery.user falls back to the owner's last-seen notification target.".into(),
|
||||||
"advanced.cooldown_secs defaults to 300.".into(),
|
"advanced.cooldown_secs defaults to 300.".into(),
|
||||||
"Legacy flat aliases are still accepted for compatibility, but grouped fields are preferred.".into(),
|
"Legacy flat aliases are still accepted for compatibility, but grouped fields are preferred.".into(),
|
||||||
@@ -852,11 +855,15 @@ fn parse_execution_mode(value: Option<String>) -> Result<NormalizedExecutionMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_routine_execution(params: &Value) -> Result<NormalizedExecutionRequest, ToolError> {
|
fn parse_routine_execution(
|
||||||
|
params: &Value,
|
||||||
|
default_use_tools: bool,
|
||||||
|
) -> Result<NormalizedExecutionRequest, ToolError> {
|
||||||
let mode = parse_execution_mode(string_field(params, "execution", "mode", &["action_type"]))?;
|
let mode = parse_execution_mode(string_field(params, "execution", "mode", &["action_type"]))?;
|
||||||
let context_paths =
|
let context_paths =
|
||||||
string_array_field(params, "execution", "context_paths", &["context_paths"]);
|
string_array_field(params, "execution", "context_paths", &["context_paths"]);
|
||||||
let use_tools = bool_field(params, "execution", "use_tools", &["use_tools"]).unwrap_or(false);
|
let use_tools =
|
||||||
|
bool_field(params, "execution", "use_tools", &["use_tools"]).unwrap_or(default_use_tools);
|
||||||
let max_tool_rounds = u64_field(params, "execution", "max_tool_rounds", &["max_tool_rounds"])
|
let max_tool_rounds = u64_field(params, "execution", "max_tool_rounds", &["max_tool_rounds"])
|
||||||
.unwrap_or(3)
|
.unwrap_or(3)
|
||||||
.clamp(1, crate::agent::routine::MAX_TOOL_ROUNDS_LIMIT as u64)
|
.clamp(1, crate::agent::routine::MAX_TOOL_ROUNDS_LIMIT as u64)
|
||||||
@@ -888,7 +895,7 @@ fn parse_routine_create_request(
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
let trigger = parse_routine_trigger(params)?;
|
let trigger = parse_routine_trigger(params)?;
|
||||||
let execution = parse_routine_execution(params)?;
|
let execution = parse_routine_execution(params, true)?;
|
||||||
let delivery = parse_routine_delivery(params);
|
let delivery = parse_routine_delivery(params);
|
||||||
let cooldown_secs =
|
let cooldown_secs =
|
||||||
u64_field(params, "advanced", "cooldown_secs", &["cooldown_secs"]).unwrap_or(300);
|
u64_field(params, "advanced", "cooldown_secs", &["cooldown_secs"]).unwrap_or(300);
|
||||||
@@ -1863,6 +1870,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_lightweight_create_with_tools_enabled_by_default() {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"name": "manual-check",
|
||||||
|
"prompt": "Inspect the repo for issues.",
|
||||||
|
"request": {
|
||||||
|
"kind": "manual"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsed = parse_routine_create_request(¶ms).expect("parse default lightweight");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(parsed.execution.mode, NormalizedExecutionMode::Lightweight),
|
||||||
|
"expected lightweight execution mode",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
parsed.execution.use_tools,
|
||||||
|
"new lightweight routines should default use_tools=true",
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.execution.max_tool_rounds, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_lightweight_create_with_explicit_tools_disabled() {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"name": "manual-check",
|
||||||
|
"prompt": "Inspect the repo for issues.",
|
||||||
|
"request": {
|
||||||
|
"kind": "manual"
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"use_tools": false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsed =
|
||||||
|
parse_routine_create_request(¶ms).expect("parse lightweight with tools disabled");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(parsed.execution.mode, NormalizedExecutionMode::Lightweight),
|
||||||
|
"expected lightweight execution mode",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!parsed.execution.use_tools,
|
||||||
|
"explicit use_tools=false should be preserved",
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.execution.max_tool_rounds, 3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_context_paths_with_trim_drop_empty_and_stable_dedupe() {
|
fn parses_context_paths_with_trim_drop_empty_and_stable_dedupe() {
|
||||||
let params = serde_json::json!({
|
let params = serde_json::json!({
|
||||||
@@ -2201,6 +2258,20 @@ mod tests {
|
|||||||
.any(|rule| rule.contains("request.kind='cron'")),
|
.any(|rule| rule.contains("request.kind='cron'")),
|
||||||
"summary should explain cron requirement",
|
"summary should explain cron requirement",
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
summary
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.any(|note| note.contains("lightweight mode with tools enabled")),
|
||||||
|
"summary should mention the new lightweight default",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
summary
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.any(|note| note.contains("execution.use_tools=false")),
|
||||||
|
"summary should mention the text-only opt-out",
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
summary
|
summary
|
||||||
.notes
|
.notes
|
||||||
|
|||||||
@@ -205,11 +205,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test 5: routine_manual_create
|
// Test 5: routine_manual_create_defaults_to_tools_enabled
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn routine_manual_create() {
|
async fn routine_manual_create_defaults_to_tools_enabled() {
|
||||||
let trace = LlmTrace::from_file(concat!(
|
let trace = LlmTrace::from_file(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/tests/fixtures/llm_traces/tools/routine_manual_create.json"
|
"/tests/fixtures/llm_traces/tools/routine_manual_create.json"
|
||||||
@@ -237,8 +237,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(routine.trigger, Trigger::Manual));
|
assert!(matches!(routine.trigger, Trigger::Manual));
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&routine.action, RoutineAction::Lightweight { use_tools, .. } if !*use_tools),
|
matches!(&routine.action, RoutineAction::Lightweight { use_tools, .. } if *use_tools),
|
||||||
"manual routine should default to lightweight without tools: {:?}",
|
"manual routine should default to lightweight with tools enabled: {:?}",
|
||||||
routine.action
|
routine.action
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -246,7 +246,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test 6: routine_history
|
// Test 6: routine_manual_create_explicit_no_tools
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn routine_manual_create_explicit_no_tools() {
|
||||||
|
let trace = LlmTrace::from_file(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/tests/fixtures/llm_traces/tools/routine_manual_create_no_tools.json"
|
||||||
|
))
|
||||||
|
.expect("failed to load routine_manual_create_no_tools.json");
|
||||||
|
|
||||||
|
let rig = TestRigBuilder::new()
|
||||||
|
.with_trace(trace.clone())
|
||||||
|
.with_auto_approve_tools(true)
|
||||||
|
.build()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
rig.send_message("Create a manual routine for quiet text-only bug triage")
|
||||||
|
.await;
|
||||||
|
let responses = rig.wait_for_responses(1, Duration::from_secs(15)).await;
|
||||||
|
|
||||||
|
rig.verify_trace_expects(&trace, &responses);
|
||||||
|
|
||||||
|
let routine = rig
|
||||||
|
.database()
|
||||||
|
.get_routine_by_name("test-user", "manual-triage-no-tools")
|
||||||
|
.await
|
||||||
|
.expect("get_routine_by_name")
|
||||||
|
.expect("manual-triage-no-tools should exist");
|
||||||
|
|
||||||
|
assert!(matches!(routine.trigger, Trigger::Manual));
|
||||||
|
assert!(
|
||||||
|
matches!(&routine.action, RoutineAction::Lightweight { use_tools, .. } if !*use_tools),
|
||||||
|
"manual routine should preserve explicit use_tools=false: {:?}",
|
||||||
|
routine.action
|
||||||
|
);
|
||||||
|
|
||||||
|
rig.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Test 7: routine_history
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -283,7 +324,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test 7: routine_system_event_emit
|
// Test 8: routine_system_event_emit
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"model_name": "test-routine-manual-create-no-tools",
|
||||||
|
"expects": {
|
||||||
|
"tools_used": ["routine_create"],
|
||||||
|
"all_tools_succeeded": true,
|
||||||
|
"min_responses": 1
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"type": "tool_calls",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_rc_manual_2",
|
||||||
|
"name": "routine_create",
|
||||||
|
"arguments": {
|
||||||
|
"name": "manual-triage-no-tools",
|
||||||
|
"trigger_type": "manual",
|
||||||
|
"prompt": "Summarize the latest bug reports when this routine is fired.",
|
||||||
|
"execution": {
|
||||||
|
"use_tools": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"input_tokens": 90,
|
||||||
|
"output_tokens": 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"type": "text",
|
||||||
|
"content": "Created the manual-triage-no-tools routine. It will only run when explicitly fired and stay text-only.",
|
||||||
|
"input_tokens": 140,
|
||||||
|
"output_tokens": 18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user