feat: add background job runner, job__* handlers, and start gates
Detached tokio::process runner with a frozen JobEnvSnapshot (env-derived bin dirs, vault-interpolated agent envs, COYOTE_TOOL_TIMEOUT resolved at start), process_group(0) with pgid-guarded SIGTERM/SIGKILL escalation, capture-only ring-buffer telemetry, and LLM_OUTPUT read after wait(). MCP jobs snapshot a single-entry McpRuntime holding only the validated server and render through the same free fn as the foreground path. job__start enforces its gates synchronously before any spawn: jobs_enabled, the backgroundable whitelist with directionality teaching errors, the per-request declared-names stash captured in before_chat_completion, then capacity (lazy supervisor get-or-init in plain sessions). job__check/list read the shared JobState cell without consuming; job__collect blocks with the escalation early-out and applies a tail-biased char-boundary cap plus optional tail_lines; job__cancel kills the group with a 5s grace. Job declarations are injected iff jobs are enabled at agent init, the plain-session function-init sites, and the exit_agent rebuild; job__ is carved out of enabled_tools filtering and excluded from concrete_tool_names so REPL toggles cannot grant or revoke it.
This commit is contained in:
+1692
-8
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ use anyhow::{Context, Result, anyhow, bail};
|
||||
use futures_util::future;
|
||||
use indexmap::IndexMap;
|
||||
use indoc::formatdoc;
|
||||
use jobs::JOB_FUNCTION_PREFIX;
|
||||
use memory::MEMORY_FUNCTION_PREFIX;
|
||||
use rag_query::RAG_FUNCTION_PREFIX;
|
||||
use rust_embed::Embed;
|
||||
@@ -625,6 +626,10 @@ impl Functions {
|
||||
.extend(supervisor::escalation_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_job_functions(&mut self) {
|
||||
self.declarations.extend(jobs::job_function_declarations());
|
||||
}
|
||||
|
||||
pub fn append_teammate_functions(&mut self) {
|
||||
self.declarations
|
||||
.extend(supervisor::teammate_function_declarations());
|
||||
@@ -1548,6 +1553,15 @@ impl ToolCall {
|
||||
json!({"tool_call_error": error_msg})
|
||||
})
|
||||
}
|
||||
_ if cmd_name.starts_with(JOB_FUNCTION_PREFIX) => {
|
||||
jobs::handle_job_tool(ctx, &cmd_name, &json_data)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
let error_msg = format!("Job tool failed: {e}");
|
||||
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️")));
|
||||
json!({"tool_call_error": error_msg})
|
||||
})
|
||||
}
|
||||
_ => match run_llm_function(cmd_name, cmd_args, envs, agent_name) {
|
||||
Ok(Some(contents)) => serde_json::from_str(&contents)
|
||||
.ok()
|
||||
@@ -2858,6 +2872,37 @@ mod tests {
|
||||
assert!(f.contains("agent__reply_escalation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn functions_append_job_adds_declarations() {
|
||||
let mut f = Functions::default();
|
||||
f.append_job_functions();
|
||||
assert!(f.contains("job__start"));
|
||||
assert!(f.contains("job__check"));
|
||||
assert!(f.contains("job__collect"));
|
||||
assert!(f.contains("job__cancel"));
|
||||
assert!(f.contains("job__list"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_routes_declared_job_calls_to_job_handlers() {
|
||||
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);
|
||||
ctx.tool_scope.functions.append_job_functions();
|
||||
let calls = vec![call("job__list", Some("id-1"))];
|
||||
let results = run_async(eval_tool_calls(&mut ctx, calls)).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].output["active_jobs"], 0);
|
||||
assert_eq!(results[0].output["jobs"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_soft_fails_job_calls_when_jobs_not_declared() {
|
||||
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);
|
||||
let calls = vec![call("job__start", Some("id-1"))];
|
||||
let results = run_async(eval_tool_calls(&mut ctx, calls)).unwrap();
|
||||
let err = results[0].output["tool_call_error"].as_str().unwrap();
|
||||
assert!(err.contains("Unexpected call"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn functions_append_teammate_adds_declarations() {
|
||||
let mut f = Functions::default();
|
||||
|
||||
Reference in New Issue
Block a user