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:
2026-08-25 17:36:17 -06:00
parent fcc3756634
commit cb025b7fff
8 changed files with 1965 additions and 23 deletions
+14 -7
View File
@@ -40,7 +40,6 @@ pub struct AgentHandle {
pub child_supervisor: Option<Arc<RwLock<Supervisor>>>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobStatus {
Running,
@@ -49,12 +48,10 @@ pub enum JobStatus {
}
pub struct JobState {
#[allow(dead_code)]
pub status: JobStatus,
pub pgid: Option<i32>,
}
#[allow(dead_code)]
pub struct JobResult {
pub output: Value,
pub exit_code: Option<i32>,
@@ -63,14 +60,11 @@ pub struct JobResult {
pub struct JobHandle {
pub id: String,
#[allow(dead_code)]
pub tool: String,
#[allow(dead_code)]
pub started_at: Instant,
pub join_handle: JoinHandle<Result<JobResult>>,
pub abort_signal: AbortSignal,
pub state: Arc<Mutex<JobState>>,
#[allow(dead_code)]
pub output_buf: Arc<Mutex<RingBuf>>,
#[allow(dead_code)]
pub no_change_checks: u32,
@@ -157,6 +151,20 @@ impl Supervisor {
})
}
pub fn job(&self, id: &str) -> Option<&JobHandle> {
match self.handles.get(id) {
Some(TaskHandle::Job(handle)) => Some(handle),
_ => None,
}
}
pub fn jobs(&self) -> impl Iterator<Item = &JobHandle> {
self.handles.values().filter_map(|handle| match handle {
TaskHandle::Job(handle) => Some(handle),
TaskHandle::Agent(_) => None,
})
}
pub fn active_count(&self) -> usize {
self.agents().count()
}
@@ -182,7 +190,6 @@ impl Supervisor {
self.max_depth
}
#[allow(dead_code)]
pub fn max_concurrent_jobs(&self) -> usize {
self.max_concurrent_jobs
}