mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-02 01:29:23 +00:00
feat: implement FullJob routine mode with scheduler dispatch (#288)
* feat: implement FullJob routine mode with scheduler dispatch FullJob routines previously fell back to lightweight mode (single LLM call, no tools) with a warning. This wires them to the existing Scheduler/Worker infrastructure so they dispatch real jobs with full tool access. Fire-and-forget model: the routine creates a job via ContextManager, schedules it, links the routine_run to the job_id, and completes immediately. The job runs independently with full tool access. - Add RoutineError::JobDispatchFailed variant - Add RoutineStore::link_routine_run_to_job (PostgreSQL + libSQL) - Add execute_full_job() in routine_engine with context_manager/scheduler - Wire context_manager + scheduler into RoutineEngine from agent_loop - Fix pre-existing clippy warnings in tests/html_to_markdown.rs Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: persist job to DB before scheduling in execute_full_job The worker emits job_actions and llm_calls rows that reference agent_jobs via foreign key. Without persisting the job first, those inserts can fail. Match the pattern from commands.rs: fetch JobContext, save_job(), then schedule. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: consolidate job dispatch into Scheduler::dispatch_job and wire max_iterations Move the create + persist + schedule sequence into a single Scheduler::dispatch_job() method so callers (commands.rs, routine_engine.rs) don't duplicate the logic. FullJob routines now pass max_iterations via job metadata, and the worker reads it (defaulting to 50 if unset). Also removes the context_manager field from RoutineEngine since dispatch_job handles everything internally. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: clamp max_iterations to 500 and log category update failures Address PR review feedback: - worker.rs: clamp max_iterations from metadata to MAX_WORKER_ITERATIONS (500) to prevent unbounded LLM token usage from malicious/buggy configs - commands.rs: log warning on category update failure instead of silently discarding the error Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat: persist worker events to DB and fix activity tab rendering In-process Worker (used by Scheduler::dispatch_job) now persists events via save_job_event at key execution points: plan creation, LLM responses, tool_use, tool_result, and job completion/failure/stuck. Event data shapes match the container worker format so the gateway activity tab renders them correctly. Frontend: tool_result errors now show a red X icon with danger styling instead of a silent empty output. The result event falls back to the error field when message is absent. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ea57447649
commit
04d3b005b1
+94
-1
@@ -98,6 +98,20 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire-and-forget persistence of a job event.
|
||||
fn log_event(&self, event_type: &str, data: serde_json::Value) {
|
||||
if let Some(store) = self.store() {
|
||||
let store = store.clone();
|
||||
let job_id = self.job_id;
|
||||
let event_type = event_type.to_string();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = store.save_job_event(job_id, &event_type, &data).await {
|
||||
tracing::warn!("Failed to persist event for job {}: {}", job_id, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the worker until the job is complete or stopped.
|
||||
pub async fn run(self, mut rx: mpsc::Receiver<WorkerMessage>) -> Result<(), Error> {
|
||||
tracing::info!("Worker starting for job {}", self.job_id);
|
||||
@@ -164,7 +178,15 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
reasoning: &Reasoning,
|
||||
reason_ctx: &mut ReasoningContext,
|
||||
) -> Result<(), Error> {
|
||||
let max_iterations = 50;
|
||||
const MAX_WORKER_ITERATIONS: usize = 500;
|
||||
let max_iterations = self
|
||||
.context_manager()
|
||||
.get_context(self.job_id)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|ctx| ctx.metadata.get("max_iterations").and_then(|v| v.as_u64()))
|
||||
.unwrap_or(50) as usize;
|
||||
let max_iterations = max_iterations.min(MAX_WORKER_ITERATIONS);
|
||||
let mut iteration = 0;
|
||||
|
||||
// Initial tool definitions for planning (will be refreshed in loop)
|
||||
@@ -193,6 +215,14 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
.join("\n")
|
||||
)));
|
||||
|
||||
self.log_event("message", serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": format!("Plan: {}\n\n{}", p.goal,
|
||||
p.actions.iter().enumerate()
|
||||
.map(|(i, a)| format!("{}. {} - {}", i + 1, a.tool_name, a.reasoning))
|
||||
.collect::<Vec<_>>().join("\n"))
|
||||
}));
|
||||
|
||||
Some(p)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -267,6 +297,14 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
// Add assistant response to context
|
||||
reason_ctx.messages.push(ChatMessage::assistant(&response));
|
||||
|
||||
self.log_event(
|
||||
"message",
|
||||
serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": response,
|
||||
}),
|
||||
);
|
||||
|
||||
// Give it one more chance to select a tool
|
||||
if iteration > 3 && iteration % 5 == 0 {
|
||||
reason_ctx.messages.push(ChatMessage::user(
|
||||
@@ -285,6 +323,16 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
tool_calls.len()
|
||||
);
|
||||
|
||||
if let Some(ref text) = content {
|
||||
self.log_event(
|
||||
"message",
|
||||
serde_json::json!({
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Add assistant message with tool_calls (OpenAI protocol)
|
||||
reason_ctx
|
||||
.messages
|
||||
@@ -667,6 +715,15 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
selection: &ToolSelection,
|
||||
result: Result<String, Error>,
|
||||
) -> Result<bool, Error> {
|
||||
self.log_event(
|
||||
"tool_use",
|
||||
serde_json::json!({
|
||||
"tool_name": selection.tool_name,
|
||||
"input": crate::agent::agent_loop::truncate_for_preview(
|
||||
&selection.parameters.to_string(), 500),
|
||||
}),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
// Sanitize output
|
||||
@@ -687,6 +744,12 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
wrapped,
|
||||
));
|
||||
|
||||
self.log_event("tool_result", serde_json::json!({
|
||||
"tool_name": selection.tool_name,
|
||||
"success": true,
|
||||
"output": crate::agent::agent_loop::truncate_for_preview(&sanitized.content, 500),
|
||||
}));
|
||||
|
||||
// Tool output never drives job completion. A malicious tool could
|
||||
// emit "TASK_COMPLETE" to force premature completion. Only the LLM's
|
||||
// own structured response (in execution_loop) can mark a job done.
|
||||
@@ -713,6 +776,15 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
});
|
||||
}
|
||||
|
||||
self.log_event(
|
||||
"tool_result",
|
||||
serde_json::json!({
|
||||
"tool_name": selection.tool_name,
|
||||
"success": false,
|
||||
"output": format!("Error: {}", e),
|
||||
}),
|
||||
);
|
||||
|
||||
reason_ctx.messages.push(ChatMessage::tool_result(
|
||||
&selection.tool_call_id,
|
||||
&selection.tool_name,
|
||||
@@ -834,6 +906,13 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
reason: s,
|
||||
})?;
|
||||
|
||||
self.log_event(
|
||||
"result",
|
||||
serde_json::json!({
|
||||
"success": true,
|
||||
"message": "Job completed successfully",
|
||||
}),
|
||||
);
|
||||
self.persist_status(
|
||||
JobState::Completed,
|
||||
Some("Job completed successfully".to_string()),
|
||||
@@ -852,6 +931,13 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
reason: s,
|
||||
})?;
|
||||
|
||||
self.log_event(
|
||||
"result",
|
||||
serde_json::json!({
|
||||
"success": false,
|
||||
"message": format!("Execution failed: {}", reason),
|
||||
}),
|
||||
);
|
||||
self.persist_status(JobState::Failed, Some(reason.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
@@ -865,6 +951,13 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
reason: s,
|
||||
})?;
|
||||
|
||||
self.log_event(
|
||||
"result",
|
||||
serde_json::json!({
|
||||
"success": false,
|
||||
"message": format!("Job stuck: {}", reason),
|
||||
}),
|
||||
);
|
||||
self.persist_status(JobState::Stuck, Some(reason.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user