Add installable agent lifecycle extensions
This commit is contained in:
102
src/agent.rs
102
src/agent.rs
@@ -40,10 +40,10 @@ struct AgentSkillMetadata {
|
||||
description: String,
|
||||
}
|
||||
|
||||
struct AgentSkill {
|
||||
name: String,
|
||||
description: String,
|
||||
path: PathBuf,
|
||||
pub(crate) struct AgentSkill {
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: String,
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
fn agent_skills_root(home: Option<&Path>) -> Option<PathBuf> {
|
||||
@@ -76,7 +76,7 @@ fn skill_frontmatter(content: &str) -> Option<&str> {
|
||||
Some(content[..end].trim_end_matches('\r'))
|
||||
}
|
||||
|
||||
fn discover_agent_skills(root: &Path) -> Vec<AgentSkill> {
|
||||
pub(crate) fn discover_agent_skills(root: &Path) -> Vec<AgentSkill> {
|
||||
let Ok(root) = root.canonicalize() else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -109,8 +109,17 @@ fn discover_agent_skills(root: &Path) -> Vec<AgentSkill> {
|
||||
skills
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn agent_skills_prompt_for(root: &Path) -> Option<String> {
|
||||
let skills = discover_agent_skills(root);
|
||||
agent_skills_prompt_for_roots(std::iter::once(root))
|
||||
}
|
||||
|
||||
fn agent_skills_prompt_for_roots<'a>(roots: impl IntoIterator<Item = &'a Path>) -> Option<String> {
|
||||
let mut skills = roots
|
||||
.into_iter()
|
||||
.flat_map(discover_agent_skills)
|
||||
.collect::<Vec<_>>();
|
||||
skills.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
if skills.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -131,9 +140,14 @@ fn agent_skills_prompt_for(root: &Path) -> Option<String> {
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn agent_skills_prompt() -> Option<String> {
|
||||
let root = agent_skills_root(std::env::var_os("HOME").as_deref().map(Path::new))?;
|
||||
agent_skills_prompt_for(&root)
|
||||
pub(crate) fn agent_skills_prompt_with(extension_roots: &[PathBuf]) -> Option<String> {
|
||||
let standard = agent_skills_root(std::env::var_os("HOME").as_deref().map(Path::new));
|
||||
agent_skills_prompt_for_roots(
|
||||
standard
|
||||
.iter()
|
||||
.map(PathBuf::as_path)
|
||||
.chain(extension_roots.iter().map(PathBuf::as_path)),
|
||||
)
|
||||
}
|
||||
|
||||
fn user_shell() -> OsString {
|
||||
@@ -1526,6 +1540,8 @@ struct RalphRuntime {
|
||||
engine: EngineSettings,
|
||||
turn: TurnSettings,
|
||||
idle_timeout: Duration,
|
||||
extensions: crate::extensions::ExtensionRegistry,
|
||||
session_id: i32,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1612,6 +1628,17 @@ impl Tools {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn enable_agent_skill_roots(&mut self, roots: &[PathBuf]) {
|
||||
self.agent_skill_roots.extend(
|
||||
roots
|
||||
.iter()
|
||||
.flat_map(|root| discover_agent_skills(root))
|
||||
.filter_map(|skill| skill.path.parent().map(Path::to_owned)),
|
||||
);
|
||||
self.agent_skill_roots.sort();
|
||||
self.agent_skill_roots.dedup();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn enable_ralph(
|
||||
&mut self,
|
||||
@@ -1620,13 +1647,17 @@ impl Tools {
|
||||
engine: EngineSettings,
|
||||
turn: TurnSettings,
|
||||
idle_timeout: Duration,
|
||||
extension_context: (crate::extensions::ExtensionRegistry, i32),
|
||||
) {
|
||||
let (extensions, session_id) = extension_context;
|
||||
self.ralph = Some(RalphRuntime {
|
||||
service,
|
||||
model,
|
||||
engine,
|
||||
turn,
|
||||
idle_timeout,
|
||||
extensions,
|
||||
session_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1786,6 +1817,53 @@ impl Tools {
|
||||
// A unique cache namespace preserves tool continuations inside this
|
||||
// round while preventing parent or earlier-round KV restoration.
|
||||
let directory = RalphRoundDirectory::create(round)?;
|
||||
let mut round_turn = runtime.turn.clone();
|
||||
let hooks = runtime.extensions.dispatch(
|
||||
&[crate::extensions::HookEvent::SubagentStart {
|
||||
session_id: runtime.session_id,
|
||||
round,
|
||||
}],
|
||||
&self.root,
|
||||
&runtime.model.to_string(),
|
||||
cancel,
|
||||
);
|
||||
let _ = runtime.extensions.persist_hook_results(&hooks);
|
||||
if let Some(status) = hooks.status() {
|
||||
send_state(
|
||||
events,
|
||||
index,
|
||||
ToolLifecycle::Running,
|
||||
Some(format!("Ralph round {round}/{max_rounds} · {status}\n")),
|
||||
);
|
||||
}
|
||||
if !hooks.errors.is_empty() {
|
||||
send_state(
|
||||
events,
|
||||
index,
|
||||
ToolLifecycle::Running,
|
||||
Some(format!(
|
||||
"Ralph round {round}/{max_rounds} · extension hook warning: {}\n",
|
||||
hooks
|
||||
.errors
|
||||
.iter()
|
||||
.map(|(id, error)| format!("{id}: {error}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
)),
|
||||
);
|
||||
}
|
||||
for output in &hooks.outputs {
|
||||
if let Some(context) = &output.additional_context {
|
||||
round_turn.system_prompt.push_str("\n\n");
|
||||
round_turn
|
||||
.system_prompt
|
||||
.push_str(&crate::extensions::wrap_context(
|
||||
&output.extension_id,
|
||||
&output.event,
|
||||
context,
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut messages = vec![ChatTurn {
|
||||
user: true,
|
||||
tool: false,
|
||||
@@ -1804,7 +1882,8 @@ impl Tools {
|
||||
"Ralph round {round}/{max_rounds} · child generation {step}\n"
|
||||
)),
|
||||
);
|
||||
let output = run_ralph_generation(runtime, &messages, &directory.0, cancel)?;
|
||||
let output =
|
||||
run_ralph_generation(runtime, &round_turn, &messages, &directory.0, cancel)?;
|
||||
let calls = parse_tool_calls(runtime.model, &output.message.content)
|
||||
.map_err(|error| format!("malformed child tool syntax: {error}"))?
|
||||
.1;
|
||||
@@ -2622,13 +2701,14 @@ impl Tools {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn run_ralph_generation(
|
||||
runtime: &RalphRuntime,
|
||||
turn: &TurnSettings,
|
||||
messages: &[ChatTurn],
|
||||
checkpoint: &Path,
|
||||
cancel: &AtomicBool,
|
||||
) -> Result<crate::engine::GenerationOutput, String> {
|
||||
let active = runtime.service.generate(
|
||||
runtime.engine.clone(),
|
||||
runtime.turn.clone(),
|
||||
turn.clone(),
|
||||
messages.to_vec(),
|
||||
CheckpointTarget::Transient(checkpoint.to_owned()),
|
||||
WorkSource::LocalChat,
|
||||
|
||||
Reference in New Issue
Block a user