use devboy_core::{PropertySchema, ToolCategory, ToolEnricher, ToolSchema};
use serde_json::Value;
pub use devboy_core::{ToolSchema as Schema, sanitize_field_name};
const LIST_TOOLS: &[&str] = &[
"get_issues",
"get_issue",
"get_issue_comments",
"get_merge_requests",
"get_merge_request",
"get_merge_request_discussions",
"get_merge_request_diffs",
"list_knowledge_base_pages",
"search_knowledge_base",
"get_knowledge_base_page",
];
fn safe_param_name(schema: &ToolSchema, preferred: &str) -> String {
if !schema.properties.contains_key(preferred) {
return preferred.to_string();
}
let mut name = format!("_{preferred}");
while schema.properties.contains_key(&name) {
name = format!("_{name}");
}
name
}
fn safe_insert(schema: &mut ToolSchema, preferred: &str, prop: PropertySchema) -> String {
let name = safe_param_name(schema, preferred);
schema.add_property(&name, prop);
name
}
pub struct FormatPipelineEnricher;
impl ToolEnricher for FormatPipelineEnricher {
fn supported_categories(&self) -> &[ToolCategory] {
&[
ToolCategory::IssueTracker,
ToolCategory::GitRepository,
ToolCategory::KnowledgeBase,
]
}
fn enrich_schema(&self, tool_name: &str, schema: &mut ToolSchema) {
if !LIST_TOOLS.contains(&tool_name) {
return;
}
safe_insert(
schema,
"format",
PropertySchema::string_enum(
&["toon", "json", "mckp"],
"Output format. \
`toon` (default) is the legacy token-optimised custom format; \
`json` is the pretty-printed baseline; \
`mckp` is the format-adaptive encoder from Paper 2 — best for \
array/object payloads on `o200k_base` tokenizers, key-lossless.",
),
);
safe_insert(
schema,
"budget",
PropertySchema::integer(
"Token budget for this response. Lower = less data + chunk index for navigation. \
Higher = more data per call. Default: from server config.",
Some(100.0),
Some(100000.0),
),
);
safe_insert(
schema,
"chunk",
PropertySchema::integer(
"Chunk number to fetch (from chunk index). \
When a response exceeds budget, it returns chunk 1 + an index of all chunks. \
Use this parameter to fetch a specific chunk by number.",
Some(1.0),
None,
),
);
}
fn transform_args(&self, _tool_name: &str, args: &mut Value) {
let _ = args;
}
}
pub type PipelineFormatEnricher = FormatPipelineEnricher;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_param_name_no_conflict() {
let schema = ToolSchema::new();
assert_eq!(safe_param_name(&schema, "budget"), "budget");
assert_eq!(safe_param_name(&schema, "chunk"), "chunk");
}
#[test]
fn test_safe_param_name_with_conflict() {
let mut schema = ToolSchema::new();
schema.add_property("chunk", PropertySchema::string("existing"));
assert_eq!(safe_param_name(&schema, "chunk"), "_chunk");
schema.add_property("_chunk", PropertySchema::string("also taken"));
assert_eq!(safe_param_name(&schema, "chunk"), "__chunk");
}
#[test]
fn test_format_pipeline_enricher_adds_params() {
let enricher = FormatPipelineEnricher;
let mut schema = ToolSchema::new();
enricher.enrich_schema("get_issues", &mut schema);
let format = schema.properties.get("format").unwrap();
assert_eq!(
format.enum_values,
Some(vec!["toon".into(), "json".into(), "mckp".into()])
);
let budget = schema.properties.get("budget").unwrap();
assert_eq!(budget.schema_type, "integer");
assert_eq!(budget.minimum, Some(100.0));
let chunk = schema.properties.get("chunk").unwrap();
assert_eq!(chunk.schema_type, "integer");
assert_eq!(chunk.minimum, Some(1.0));
assert!(!schema.properties.contains_key("offset"));
assert!(!schema.properties.contains_key("limit"));
}
#[test]
fn test_enricher_skips_non_list_tools() {
let enricher = FormatPipelineEnricher;
let mut schema = ToolSchema::new();
enricher.enrich_schema("create_issue", &mut schema);
assert!(schema.properties.is_empty());
}
#[test]
fn test_enricher_safe_naming_on_collision() {
let enricher = FormatPipelineEnricher;
let mut schema = ToolSchema::new();
schema.add_property("chunk", PropertySchema::string("tool's own chunk param"));
enricher.enrich_schema("get_merge_request_diffs", &mut schema);
let original = schema.properties.get("chunk").unwrap();
assert_eq!(original.schema_type, "string");
let enriched = schema.properties.get("_chunk").unwrap();
assert_eq!(enriched.schema_type, "integer");
assert!(schema.properties.contains_key("format"));
assert!(schema.properties.contains_key("budget"));
}
#[test]
fn test_enricher_categories() {
let enricher = FormatPipelineEnricher;
let cats = enricher.supported_categories();
assert!(cats.contains(&ToolCategory::IssueTracker));
assert!(cats.contains(&ToolCategory::GitRepository));
assert!(cats.contains(&ToolCategory::KnowledgeBase));
}
#[test]
fn test_format_pipeline_enricher_covers_kb_tools() {
let enricher = FormatPipelineEnricher;
let mut schema = ToolSchema::new();
enricher.enrich_schema("search_knowledge_base", &mut schema);
assert!(schema.properties.contains_key("format"));
assert!(schema.properties.contains_key("budget"));
assert!(schema.properties.contains_key("chunk"));
}
}