mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-02 09:39:37 +00:00
Replace hardcoded intent patterns with job tools
Remove the brittle natural language pattern matching from the router and add job management tools to the normal tool registry instead. - Add job tools: create_job, list_jobs, job_status, cancel_job - Router now only handles explicit /commands - Natural language goes through agentic loop with all tools - LLM naturally picks appropriate tools based on user intent - Share ContextManager between job tools and Agent Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
27ffc12f6c
commit
7baf9e379d
@@ -0,0 +1,417 @@
|
||||
//! Job management tools.
|
||||
//!
|
||||
//! These tools allow the LLM to manage jobs:
|
||||
//! - Create new jobs/tasks
|
||||
//! - List existing jobs
|
||||
//! - Check job status
|
||||
//! - Cancel running jobs
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{ContextManager, JobContext, JobState};
|
||||
use crate::tools::tool::{Tool, ToolError, ToolOutput};
|
||||
|
||||
/// Tool for creating a new job.
|
||||
pub struct CreateJobTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl CreateJobTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for CreateJobTool {
|
||||
fn name(&self) -> &str {
|
||||
"create_job"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Create a new job or task for the agent to work on. Use this when the user wants \
|
||||
you to do something substantial that should be tracked as a separate job."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A short title for the job (max 100 chars)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Full description of what needs to be done"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let title = params
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'title' parameter".into()))?;
|
||||
|
||||
let description = params
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
ToolError::InvalidParameters("missing 'description' parameter".into())
|
||||
})?;
|
||||
|
||||
match self.context_manager.create_job(title, description).await {
|
||||
Ok(job_id) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": title,
|
||||
"status": "pending",
|
||||
"message": format!("Created job '{}'", title)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": e.to_string()
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for listing jobs.
|
||||
pub struct ListJobsTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl ListJobsTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for ListJobsTool {
|
||||
fn name(&self) -> &str {
|
||||
"list_jobs"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"List all jobs or filter by status. Shows job IDs, titles, and current status."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"type": "string",
|
||||
"description": "Filter by status: 'active', 'completed', 'failed', 'all' (default: 'all')",
|
||||
"enum": ["active", "completed", "failed", "all"]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let filter = params
|
||||
.get("filter")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("all");
|
||||
|
||||
let job_ids = match filter {
|
||||
"active" => self.context_manager.active_jobs().await,
|
||||
_ => self.context_manager.all_jobs().await,
|
||||
};
|
||||
|
||||
let mut jobs = Vec::new();
|
||||
for job_id in job_ids {
|
||||
if let Ok(ctx) = self.context_manager.get_context(job_id).await {
|
||||
let include = match filter {
|
||||
"completed" => ctx.state == JobState::Completed,
|
||||
"failed" => ctx.state == JobState::Failed,
|
||||
"active" => ctx.state.is_active(),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if include {
|
||||
jobs.push(serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": ctx.title,
|
||||
"status": format!("{:?}", ctx.state),
|
||||
"created_at": ctx.created_at.to_rfc3339()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let summary = self.context_manager.summary().await;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"jobs": jobs,
|
||||
"summary": {
|
||||
"total": summary.total,
|
||||
"pending": summary.pending,
|
||||
"in_progress": summary.in_progress,
|
||||
"completed": summary.completed,
|
||||
"failed": summary.failed
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for checking job status.
|
||||
pub struct JobStatusTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl JobStatusTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for JobStatusTool {
|
||||
fn name(&self) -> &str {
|
||||
"job_status"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Check the status and details of a specific job by its ID."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the job to check"
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let job_id_str = params
|
||||
.get("job_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
|
||||
|
||||
let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
|
||||
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
|
||||
})?;
|
||||
|
||||
match self.context_manager.get_context(job_id).await {
|
||||
Ok(ctx) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"title": ctx.title,
|
||||
"description": ctx.description,
|
||||
"status": format!("{:?}", ctx.state),
|
||||
"created_at": ctx.created_at.to_rfc3339(),
|
||||
"started_at": ctx.started_at.map(|t| t.to_rfc3339()),
|
||||
"completed_at": ctx.completed_at.map(|t| t.to_rfc3339()),
|
||||
"actual_cost": ctx.actual_cost.to_string()
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Job not found: {}", e)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool for canceling a job.
|
||||
pub struct CancelJobTool {
|
||||
context_manager: Arc<ContextManager>,
|
||||
}
|
||||
|
||||
impl CancelJobTool {
|
||||
pub fn new(context_manager: Arc<ContextManager>) -> Self {
|
||||
Self { context_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for CancelJobTool {
|
||||
fn name(&self) -> &str {
|
||||
"cancel_job"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Cancel a running or pending job. The job will be marked as cancelled and stopped."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the job to cancel"
|
||||
}
|
||||
},
|
||||
"required": ["job_id"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let job_id_str = params
|
||||
.get("job_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
|
||||
|
||||
let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
|
||||
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
|
||||
})?;
|
||||
|
||||
// Transition to cancelled state
|
||||
match self
|
||||
.context_manager
|
||||
.update_context(job_id, |ctx| {
|
||||
ctx.transition_to(JobState::Cancelled, Some("Cancelled by user".to_string()))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
let result = serde_json::json!({
|
||||
"job_id": job_id.to_string(),
|
||||
"status": "cancelled",
|
||||
"message": "Job cancelled successfully"
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Ok(Err(reason)) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Cannot cancel job: {}", reason)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
Err(e) => {
|
||||
let result = serde_json::json!({
|
||||
"error": format!("Job not found: {}", e)
|
||||
});
|
||||
Ok(ToolOutput::success(result, start.elapsed()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn requires_approval(&self) -> bool {
|
||||
true // Canceling a job should require approval
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_job_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
let tool = CreateJobTool::new(manager.clone());
|
||||
|
||||
let params = serde_json::json!({
|
||||
"title": "Test Job",
|
||||
"description": "A test job description"
|
||||
});
|
||||
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
let job_id = result.result.get("job_id").unwrap().as_str().unwrap();
|
||||
assert!(!job_id.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_jobs_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
|
||||
// Create some jobs
|
||||
manager.create_job("Job 1", "Desc 1").await.unwrap();
|
||||
manager.create_job("Job 2", "Desc 2").await.unwrap();
|
||||
|
||||
let tool = ListJobsTool::new(manager);
|
||||
|
||||
let params = serde_json::json!({});
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
let jobs = result.result.get("jobs").unwrap().as_array().unwrap();
|
||||
assert_eq!(jobs.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_job_status_tool() {
|
||||
let manager = Arc::new(ContextManager::new(5));
|
||||
let job_id = manager.create_job("Test Job", "Description").await.unwrap();
|
||||
|
||||
let tool = JobStatusTool::new(manager);
|
||||
|
||||
let params = serde_json::json!({
|
||||
"job_id": job_id.to_string()
|
||||
});
|
||||
let ctx = JobContext::default();
|
||||
let result = tool.execute(params, &ctx).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.result.get("title").unwrap().as_str().unwrap(),
|
||||
"Test Job"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod echo;
|
||||
mod ecommerce;
|
||||
mod file;
|
||||
mod http;
|
||||
mod job;
|
||||
mod json;
|
||||
mod marketplace;
|
||||
mod memory;
|
||||
@@ -16,6 +17,7 @@ pub use echo::EchoTool;
|
||||
pub use ecommerce::EcommerceTool;
|
||||
pub use file::{ApplyPatchTool, ListDirTool, ReadFileTool, WriteFileTool};
|
||||
pub use http::HttpTool;
|
||||
pub use job::{CancelJobTool, CreateJobTool, JobStatusTool, ListJobsTool};
|
||||
pub use json::JsonTool;
|
||||
pub use marketplace::MarketplaceTool;
|
||||
pub use memory::{MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool};
|
||||
|
||||
+17
-2
@@ -5,12 +5,14 @@ use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::context::ContextManager;
|
||||
use crate::llm::{LlmProvider, ToolDefinition};
|
||||
use crate::safety::SafetyLayer;
|
||||
use crate::tools::builder::{BuildSoftwareTool, BuilderConfig, LlmSoftwareBuilder};
|
||||
use crate::tools::builtin::{
|
||||
ApplyPatchTool, EchoTool, HttpTool, JsonTool, ListDirTool, MemoryReadTool, MemorySearchTool,
|
||||
MemoryTreeTool, MemoryWriteTool, ReadFileTool, ShellTool, TimeTool, WriteFileTool,
|
||||
ApplyPatchTool, CancelJobTool, CreateJobTool, EchoTool, HttpTool, JobStatusTool, JsonTool,
|
||||
ListDirTool, ListJobsTool, MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool,
|
||||
ReadFileTool, ShellTool, TimeTool, WriteFileTool,
|
||||
};
|
||||
use crate::tools::tool::Tool;
|
||||
use crate::tools::wasm::{
|
||||
@@ -144,6 +146,19 @@ impl ToolRegistry {
|
||||
tracing::info!("Registered 4 memory tools");
|
||||
}
|
||||
|
||||
/// Register job management tools.
|
||||
///
|
||||
/// Job tools allow the LLM to create, list, check status, and cancel jobs.
|
||||
/// These enable natural language job management without hardcoded intent parsing.
|
||||
pub fn register_job_tools(&self, context_manager: Arc<ContextManager>) {
|
||||
self.register_sync(Arc::new(CreateJobTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(ListJobsTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(JobStatusTool::new(Arc::clone(&context_manager))));
|
||||
self.register_sync(Arc::new(CancelJobTool::new(context_manager)));
|
||||
|
||||
tracing::info!("Registered 4 job management tools");
|
||||
}
|
||||
|
||||
/// Register the software builder tool.
|
||||
///
|
||||
/// The builder tool allows the agent to create new software including WASM tools,
|
||||
|
||||
Reference in New Issue
Block a user