1use std::collections::HashSet;
31use std::sync::Arc;
32
33use pi_agent::AgentTool;
34
35use super::AgentSession;
36
37#[derive(Clone, Debug, PartialEq)]
39pub struct ToolInfo {
40 pub name: String,
42 pub description: String,
44 pub parameters: serde_json::Value,
46}
47
48#[derive(Clone, Debug, Default)]
50pub struct RefreshToolRegistryOptions {
51 pub active_tool_names: Option<Vec<String>>,
54 pub include_all_extension_tools: bool,
57}
58
59impl AgentSession {
60 pub(super) fn build_initial_tool_registry(
66 &self,
67 base_tools: Vec<Arc<dyn AgentTool>>,
68 initial_active: Option<Vec<String>>,
69 allowed: Option<Vec<String>>,
70 excluded: Option<Vec<String>>,
71 ) {
72 {
73 let mut inner = self.lock_inner();
74 inner.allowed_tool_names = allowed.map(|names| names.into_iter().collect());
75 inner.excluded_tool_names = excluded.map(|names| names.into_iter().collect());
76 inner.base_tool_definitions = build_base_definitions(base_tools);
77 inner.tool_registry.clear();
78 }
79 let opts = RefreshToolRegistryOptions {
80 active_tool_names: initial_active,
81 include_all_extension_tools: true,
82 };
83 self.refresh_tool_registry(&opts);
84 }
85
86 pub fn refresh_tool_registry(&self, options: &RefreshToolRegistryOptions) {
97 let runner = self.hooks.runner();
98 let extension_tools = runner.get_all_registered_tools();
99 let (base_definitions, allowed, excluded, previous_active, previous_registry_names) = {
100 let inner = self.lock_inner();
101 (
102 inner.base_tool_definitions.clone(),
103 inner.allowed_tool_names.clone(),
104 inner.excluded_tool_names.clone(),
105 self.agent_state_tool_names(),
106 inner
107 .tool_registry
108 .iter()
109 .map(|entry| entry.name().to_owned())
110 .collect::<HashSet<String>>(),
111 )
112 };
113 let is_allowed = |name: &str| {
114 allowed.as_ref().is_none_or(|set| set.contains(name))
115 && !excluded.as_ref().is_some_and(|set| set.contains(name))
116 };
117
118 let mut registry: Vec<Arc<dyn AgentTool>> =
120 Vec::with_capacity(base_definitions.len().saturating_add(extension_tools.len()));
121 let mut seen: HashSet<String> = HashSet::new();
122 for tool in &base_definitions {
123 let name = tool.name();
124 if is_allowed(name) && seen.insert(name.to_owned()) {
125 registry.push(Arc::clone(tool));
126 }
127 }
128 let mut extension_pairs: Vec<(String, Arc<dyn AgentTool>)> = extension_tools
133 .into_iter()
134 .filter(|(name, _)| is_allowed(name))
135 .collect();
136 extension_pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
137 for (name, tool) in extension_pairs {
138 if seen.insert(name.clone()) {
139 registry.push(tool);
140 }
141 }
142
143 let mut next_active: Vec<String> = match &options.active_tool_names {
145 Some(names) => names.clone(),
146 None => previous_active.clone(),
147 };
148 next_active.retain(|name| is_allowed(name));
149 if let Some(allowed_set) = &allowed {
150 for entry in ®istry {
151 if allowed_set.contains(entry.name()) {
152 next_active.push(entry.name().to_owned());
153 }
154 }
155 } else if options.include_all_extension_tools {
156 for entry in ®istry {
157 let name = entry.name();
158 if !base_definitions.iter().any(|base| base.name() == name) {
159 next_active.push(name.to_owned());
160 }
161 }
162 } else if options.active_tool_names.is_none() {
163 for entry in ®istry {
164 let name = entry.name();
165 if !previous_registry_names.contains(name) {
166 next_active.push(name.to_owned());
167 }
168 }
169 }
170
171 {
174 let mut inner = self.lock_inner();
175 inner.tool_registry = registry;
176 }
177 let deduped = dedup_preserve_order(next_active);
178 self.set_active_tools_by_name(deduped);
179 }
180
181 #[must_use]
183 pub fn get_active_tool_names(&self) -> Vec<String> {
184 self.agent_state_tool_names()
185 }
186
187 #[must_use]
192 pub fn get_all_tools(&self) -> Vec<ToolInfo> {
193 self.lock_inner()
194 .tool_registry
195 .iter()
196 .map(|tool| ToolInfo {
197 name: tool.name().to_owned(),
198 description: tool.description().to_owned(),
199 parameters: tool.parameters().clone(),
200 })
201 .collect()
202 }
203
204 #[must_use]
206 pub fn get_tool(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
207 self.lock_inner()
208 .tool_registry
209 .iter()
210 .find(|tool| tool.name() == name)
211 .cloned()
212 }
213
214 pub fn set_active_tools_by_name(&self, tool_names: Vec<String>) {
224 let registry = self.lock_inner().tool_registry.clone();
225 let lookup = |name: &str| -> Option<Arc<dyn AgentTool>> {
226 registry.iter().find(|tool| tool.name() == name).cloned()
227 };
228 let mut tools: Vec<Arc<dyn AgentTool>> = Vec::with_capacity(tool_names.len());
229 let mut valid_names: Vec<String> = Vec::with_capacity(tool_names.len());
230 let mut seen: HashSet<String> = HashSet::new();
231 for name in tool_names {
232 if seen.contains(&name) {
233 continue;
234 }
235 if let Some(tool) = lookup(&name) {
236 tools.push(tool);
237 valid_names.push(name.clone());
238 seen.insert(name);
239 }
240 }
241 self.hooks.set_tools(tools.clone());
244 self.agent.set_tools(tools);
245 {
246 let mut inner = self.lock_inner();
247 inner.active_tool_names = valid_names;
248 }
249 }
250
251 fn agent_state_tool_names(&self) -> Vec<String> {
253 self.agent
254 .state()
255 .tools
256 .iter()
257 .map(|tool| tool.name().to_owned())
258 .collect()
259 }
260}
261
262#[derive(Clone, Debug, PartialEq, Eq)]
271pub struct ParsedSkillBlock {
272 pub name: String,
274 pub location: String,
276 pub content: String,
278 pub user_message: Option<String>,
280}
281
282#[must_use]
288pub fn parse_skill_block(text: &str) -> Option<ParsedSkillBlock> {
289 let prefix = "<skill name=\"";
290 let after_name_open = text.strip_prefix(prefix)?;
291 let name_attr_close = "\" location=\"";
293 let name_end = after_name_open.find(name_attr_close)?;
294 let name = &after_name_open[..name_end];
295 let after_location_open = &after_name_open[name_end + name_attr_close.len()..];
296 let location_end = after_location_open.find("\">")?;
298 let location = &after_location_open[..location_end];
299 let after_open_tag = &after_location_open[location_end + "\">".len()..];
300 let body = after_open_tag.strip_prefix('\n')?;
302 let close_tag = "\n</skill>";
303 let body_end = body.find(close_tag)?;
304 let content = &body[..body_end];
305 let trailing = &body[body_end + close_tag.len()..];
306 let user_message = match trailing.strip_prefix("\n\n") {
307 Some(rest) => {
308 let trimmed = rest.trim();
309 if trimmed.is_empty() {
310 None
311 } else {
312 Some(trimmed.to_owned())
313 }
314 }
315 None => {
316 if trailing.is_empty() {
319 None
320 } else {
321 return None;
322 }
323 }
324 };
325 Some(ParsedSkillBlock {
326 name: name.to_owned(),
327 location: location.to_owned(),
328 content: content.to_owned(),
329 user_message,
330 })
331}
332
333fn build_base_definitions(tools: Vec<Arc<dyn AgentTool>>) -> Vec<Arc<dyn AgentTool>> {
342 let mut seen: HashSet<String> = HashSet::with_capacity(tools.len());
343 let mut out: Vec<Arc<dyn AgentTool>> = Vec::with_capacity(tools.len());
344 for tool in tools {
345 if seen.insert(tool.name().to_owned()) {
346 out.push(tool);
347 }
348 }
349 out
350}
351
352fn dedup_preserve_order(names: Vec<String>) -> Vec<String> {
354 let mut seen: HashSet<String> = HashSet::with_capacity(names.len());
355 let mut out = Vec::with_capacity(names.len());
356 for name in names {
357 if seen.contains(&name) {
358 continue;
359 }
360 seen.insert(name.clone());
361 out.push(name);
362 }
363 out
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use serde_json::Value;
370
371 struct StubTool {
372 name: String,
373 description: String,
374 parameters: Value,
375 }
376
377 impl StubTool {
378 fn new_arc(name: &str) -> Arc<Self> {
379 Arc::new(Self {
380 name: name.to_owned(),
381 description: format!("stub {name}"),
382 parameters: serde_json::json!({ "type": "object" }),
383 })
384 }
385 }
386
387 impl AgentTool for StubTool {
388 fn name(&self) -> &str {
389 &self.name
390 }
391 fn label(&self) -> &str {
392 &self.name
393 }
394 fn description(&self) -> &str {
395 &self.description
396 }
397 fn parameters(&self) -> &Value {
398 &self.parameters
399 }
400 fn validate_arguments(
401 &self,
402 args: &serde_json::Map<String, Value>,
403 ) -> Result<serde_json::Map<String, Value>, pi_agent::ToolError> {
404 Ok(args.clone())
405 }
406 fn execute(
407 &self,
408 _tool_call_id: &str,
409 _args: serde_json::Map<String, Value>,
410 _cancel: tokio_util::sync::CancellationToken,
411 _updates: pi_agent::ToolUpdates,
412 ) -> futures::future::BoxFuture<
413 'static,
414 Result<pi_agent::AgentToolResult, pi_agent::ToolError>,
415 > {
416 Box::pin(async { Ok(pi_agent::AgentToolResult::default()) })
417 }
418 }
419
420 #[test]
421 fn build_base_definitions_first_wins_and_preserves_order() {
422 let a = StubTool::new_arc("read");
423 let b = StubTool::new_arc("bash");
424 let dup = StubTool::new_arc("read");
425 let map = build_base_definitions(vec![a, b, dup]);
426 assert_eq!(map.len(), 2);
427 assert_eq!(map[0].name(), "read");
428 assert_eq!(map[1].name(), "bash");
429 }
430
431 #[test]
432 fn dedup_preserve_order_keeps_first() {
433 let names = vec![
434 "read".to_owned(),
435 "bash".to_owned(),
436 "read".to_owned(),
437 "edit".to_owned(),
438 ];
439 assert_eq!(dedup_preserve_order(names), vec!["read", "bash", "edit"]);
440 }
441
442 #[test]
443 fn tool_info_carries_name_description_parameters() {
444 let tool = StubTool::new_arc("grep");
445 let info = ToolInfo {
446 name: tool.name().to_owned(),
447 description: tool.description().to_owned(),
448 parameters: tool.parameters().clone(),
449 };
450 assert_eq!(info.name, "grep");
451 assert_eq!(info.description, "stub grep");
452 assert_eq!(info.parameters, serde_json::json!({ "type": "object" }));
453 }
454
455 #[test]
456 fn parses_simple_skill_block_without_user_message() -> Result<(), &'static str> {
457 let text = "<skill name=\"commit\" location=\"/sk/commit.md\">\nbody\n</skill>";
458 let parsed = parse_skill_block(text).ok_or("skill block should parse")?;
459 assert_eq!(parsed.name, "commit");
460 assert_eq!(parsed.location, "/sk/commit.md");
461 assert_eq!(parsed.content, "body");
462 assert!(parsed.user_message.is_none());
463 Ok(())
464 }
465
466 #[test]
467 fn parses_skill_block_with_user_message() -> Result<(), &'static str> {
468 let text =
469 "<skill name=\"commit\" location=\"/sk/commit.md\">\nbody\n</skill>\n\nfix the bug";
470 let parsed = parse_skill_block(text).ok_or("skill block should parse")?;
471 assert_eq!(parsed.user_message.as_deref(), Some("fix the bug"));
472 Ok(())
473 }
474
475 #[test]
476 fn returns_none_for_non_skill_text() {
477 assert!(parse_skill_block("hello world").is_none());
478 assert!(parse_skill_block("<other>").is_none());
479 }
480
481 #[test]
482 fn returns_none_for_trailing_garbage() {
483 let text = "<skill name=\"a\" location=\"b\">\nc\n</skill>extra";
484 assert!(parse_skill_block(text).is_none());
485 }
486
487 #[test]
488 fn parses_multi_line_body() -> Result<(), &'static str> {
489 let text = "<skill name=\"a\" location=\"b\">\nline1\nline2\nline3\n</skill>";
490 let parsed = parse_skill_block(text).ok_or("skill block should parse")?;
491 assert_eq!(parsed.content, "line1\nline2\nline3");
492 Ok(())
493 }
494
495 #[test]
496 fn empty_user_message_after_separator_is_none() -> Result<(), &'static str> {
497 let text = "<skill name=\"a\" location=\"b\">\nbody\n</skill>\n\n ";
498 let parsed = parse_skill_block(text).ok_or("skill block should parse")?;
499 assert!(parsed.user_message.is_none());
500 Ok(())
501 }
502
503 #[test]
504 fn round_trips_with_expand_format() -> Result<(), &'static str> {
505 let text = "<skill name=\"commit\" location=\"/sk/commit.md\">\nReferences are relative to /sk.\n\nBody here\n</skill>";
507 let parsed = parse_skill_block(text).ok_or("skill block should parse")?;
508 assert_eq!(parsed.name, "commit");
509 assert_eq!(
510 parsed.content,
511 "References are relative to /sk.\n\nBody here"
512 );
513 Ok(())
514 }
515}