loopd: subagent spawn on the background slot
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user