loopd: subagent spawn on the background slot

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-03 19:59:25 -07:00
co-authored by ornith-1.5-35b-a3b
parent fe145fb83a
commit 4491e1b80d
13 changed files with 471 additions and 12 deletions
+1
View File
@@ -146,6 +146,7 @@ pub fn handle(stream: UnixStream, ctx: Arc<Context>) {
client: &ctx.client,
port: &*ctx.port,
registry: &ctx.registry,
ctx: Some(&*ctx),
};
let mut session = if turn.resume {
+2
View File
@@ -136,6 +136,7 @@ impl Scheduler {
client: &ctx.client,
port: &*ctx.port,
registry: &ctx.registry,
ctx: Some(ctx),
};
let text = format!("It is {}.\n\n{}", now.to_rfc3339(), job.prompt);
let mut emit = |_event: &TurnEvent| {};
@@ -247,6 +248,7 @@ impl Scheduler {
client: &ctx.client,
port: &*ctx.port,
registry: &ctx.registry,
ctx: Some(ctx),
};
let text = format!("It is {}.\n\n{file}", now.to_rfc3339());
+118 -12
View File
@@ -1,8 +1,20 @@
//! The `subagent` tool: loopd answers it itself (M6 spec, section 3).
//! It is not core; the parent finds it with `find_tool` and calls it through `call_tool`.
use crate::baseline::Baseline;
use crate::channel::Busy;
use crate::compact;
use crate::session::{Session, SessionError};
use crate::tools::{cap_result, denial_message};
use crate::turn::{Runtime, run_turn};
use bxcore::sanitize::neutralise;
use bxcore::{CallId, DataClass, DenyReason, SessionId, Timestamp, TurnEvent};
pub const SUBAGENT: &str = "subagent";
/// What `subagent` returns when a child session id would not fit a `SessionId`.
pub const RUNBOOK: &str = "see docs/runbook.md#subagent-id";
pub fn schema() -> crate::llama::ToolSchema {
crate::llama::ToolSchema {
name: SUBAGENT.to_string(),
@@ -62,18 +74,112 @@ pub fn dispatch_args(arguments: &str) -> crate::tools::Dispatch {
}
}
/// Spawn is task 03. The match arm in `run_call` needs this name now.
/// Spawn the child, run one turn on the background slot, and return the parent's tool text.
pub fn run(
rt: &crate::turn::Runtime<'_>,
parent: &crate::session::Session,
rt: &Runtime<'_>,
parent: &Session,
task: &str,
call_id: bxcore::CallId,
on_event: &mut dyn FnMut(&bxcore::TurnEvent),
) -> (String, bxcore::DataClass, bool) {
let _ = (rt, parent, task, call_id, on_event);
(
"subagent stub".to_string(),
bxcore::DataClass::Public,
false,
)
call_id: CallId,
on_event: &mut dyn FnMut(&TurnEvent),
) -> (String, DataClass, bool) {
let raw = format!("sub-{}-{}", parent.id().as_str(), call_id.0);
let child = match SessionId::new(&raw) {
Ok(id) => id,
Err(_) => {
return (
denial_message(
DenyReason::InvalidArguments,
Some(&format!(
"child session id `{raw}` would not fit; {RUNBOOK}"
)),
),
DataClass::Public,
false,
);
}
};
let Some(ctx) = rt.ctx else {
return failed(&child);
};
let held = match ctx.hold(child.clone()) {
Ok(held) => held,
Err(Busy) => return failed(&child),
};
let block = match ctx.memory.block() {
Ok(block) => block,
Err(_) => {
drop(held);
return failed(&child);
}
};
let baseline = match Baseline::assemble_with(rt.cfg, rt.registry, &block) {
Ok(baseline) => baseline,
Err(_) => {
drop(held);
return failed(&child);
}
};
let (task, _) = cap_result(task, rt.cfg.r#loop.tool_result_cap);
let text = format!("It is {}.\n\n{task}", Timestamp::now().to_rfc3339());
let mut session = match Session::create(
&rt.cfg.paths.home,
child.clone(),
baseline,
rt.cfg.slots.background,
) {
Ok(session) => session,
Err(SessionError::Exists(_)) => match Session::open(&rt.cfg.paths.home, child.clone()) {
Ok(session) => session,
Err(_) => {
drop(held);
return failed(&child);
}
},
Err(_) => {
drop(held);
return failed(&child);
}
};
let outcome = run_turn(&mut session, rt, &text, None, on_event);
let outcome = compact::finish_turn(
&mut session,
rt,
&*ctx.memory,
&text,
None,
on_event,
outcome,
);
let _ = ctx.memory.updated(session.id(), session.turns());
let untrusted = session.untrusted();
drop(held);
match outcome {
Ok(done) => {
let summary = compact::cap_chars(
neutralise(&done.content).as_ref(),
rt.cfg.compact.summary_chars,
);
(
format!("{} completed\n{summary}", child.as_str()),
DataClass::Public,
untrusted,
)
}
Err(_) => (
format!("{} failed", child.as_str()),
DataClass::Public,
untrusted,
),
}
}
fn failed(id: &SessionId) -> (String, DataClass, bool) {
(format!("{} failed", id.as_str()), DataClass::Public, false)
}
+3
View File
@@ -57,6 +57,9 @@ pub struct Runtime<'a> {
pub client: &'a Client,
pub port: &'a dyn ToolPort,
pub registry: &'a Registry,
/// Present on a live `loopd` turn so `subagent` can hold the child. Tests that do not spawn
/// leave this `None`.
pub ctx: Option<&'a crate::channel::Context>,
}
/// True for the one 400 the server sends when the prompt does not fit.