1pub mod bash;
9mod edit;
10mod edit_diff;
11
12pub use edit_diff::{Replacement, preview_edits};
13pub mod external;
14mod find;
15mod grep;
16mod ls;
17pub mod mutation;
18pub mod path;
19mod read;
20pub mod registry;
21pub mod truncate;
22mod write;
23
24use std::path::PathBuf;
25use std::sync::{Arc, RwLock};
26
27use futures_util::future::BoxFuture;
28use serde_json::Value;
29use tokio_util::sync::CancellationToken;
30use yapi_agent::{Tool, UpdateSink};
31use yapi_types::event::ToolResult;
32use yapi_types::message::{ContentBlock, ThinkingLevel, ToolDeclaration};
33use yapi_types::model::Model;
34
35pub use bash::Bash;
36pub use edit::Edit;
37pub use find::Find;
38pub use grep::Grep;
39pub use ls::Ls;
40pub use read::{Read, image_mime_type};
41pub use write::Write;
42
43pub const DEFAULT_TOOLS: [&str; 4] = ["read", "bash", "edit", "write"];
45
46pub const BUILTIN_TOOLS: [&str; 7] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
48
49#[derive(Clone, Debug)]
51pub struct Runtime {
52 pub model: Option<Model>,
54 pub thinking_level: Option<ThinkingLevel>,
56 pub session_id: Option<String>,
58 pub session_file: Option<PathBuf>,
60 pub auto_resize_images: bool,
62 pub shell_path: Option<String>,
64 pub shell_command_prefix: Option<String>,
66}
67
68impl Default for Runtime {
69 fn default() -> Runtime {
71 Runtime {
72 model: None,
73 thinking_level: None,
74 session_id: None,
75 session_file: None,
76 auto_resize_images: true,
77 shell_path: None,
78 shell_command_prefix: None,
79 }
80 }
81}
82
83#[derive(Clone, Debug)]
86pub struct ToolEnv {
87 pub cwd: PathBuf,
89 pub runtime: Arc<RwLock<Runtime>>,
91 pub bin_dir: PathBuf,
94}
95
96impl ToolEnv {
97 pub fn runtime(&self) -> Runtime {
99 yapi_types::sync::read(&self.runtime).clone()
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105#[serde(rename_all = "kebab-case")]
106pub enum Exposure {
107 Direct,
109 ModelOnly,
111 Codemode,
113 Deferred,
115 Hidden,
117}
118
119impl Exposure {
120 pub fn declarable(self) -> bool {
122 matches!(self, Exposure::Direct | Exposure::ModelOnly)
123 }
124}
125
126#[derive(Clone, Debug, Default, PartialEq, Eq)]
128pub struct Namespace {
129 pub name: String,
131 pub description: Option<String>,
133 pub instructions: Option<String>,
135}
136
137pub struct Described {
139 tool: Arc<dyn Tool>,
140 declaration: ToolDeclaration,
141}
142
143impl Described {
144 pub fn new(tool: Arc<dyn Tool>, description: String) -> Described {
146 let declaration = ToolDeclaration {
147 description,
148 ..tool.declaration().clone()
149 };
150 Described { tool, declaration }
151 }
152}
153
154impl Tool for Described {
155 fn declaration(&self) -> &ToolDeclaration {
156 &self.declaration
157 }
158
159 fn label(&self) -> &str {
160 self.tool.label()
161 }
162
163 fn execution_mode(&self) -> yapi_agent::ExecutionMode {
164 self.tool.execution_mode()
165 }
166
167 fn output_schema(&self) -> Option<&Value> {
168 self.tool.output_schema()
169 }
170
171 fn prepare_arguments(
172 &self,
173 arguments: serde_json::Map<String, Value>,
174 ) -> serde_json::Map<String, Value> {
175 self.tool.prepare_arguments(arguments)
176 }
177
178 fn execute(
179 &self,
180 call_id: String,
181 args: Value,
182 cancel: CancellationToken,
183 updates: UpdateSink,
184 ) -> BoxFuture<'_, Result<ToolResult, String>> {
185 self.tool.execute(call_id, args, cancel, updates)
186 }
187}
188
189#[derive(Clone)]
192pub struct RegisteredTool {
193 pub tool: Arc<dyn Tool>,
195 pub snippet: Option<String>,
198 pub guidelines: Vec<String>,
200 pub exposure: Exposure,
202 pub namespace: Option<Namespace>,
204 pub default_active: bool,
206 pub annotations: Option<Value>,
208 pub source: Option<yapi_types::rpc::SourceInfo>,
210}
211
212impl RegisteredTool {
213 pub fn direct(tool: Arc<dyn Tool>, snippet: Option<String>, guidelines: Vec<String>) -> Self {
215 RegisteredTool {
216 tool,
217 snippet,
218 guidelines,
219 exposure: Exposure::Direct,
220 namespace: None,
221 default_active: true,
222 annotations: None,
223 source: None,
224 }
225 }
226
227 pub fn name(&self) -> &str {
229 &self.tool.declaration().name
230 }
231
232 pub fn info(&self) -> crate::extensions::ToolInfo {
234 let declaration = self.tool.declaration();
235 crate::extensions::ToolInfo {
236 name: declaration.name.clone(),
237 description: declaration.description.clone(),
238 parameters: declaration.parameters.clone(),
239 exposure: self.exposure,
240 namespace: self.namespace.clone(),
241 }
242 }
243}
244
245pub fn builtin(name: &str, env: &ToolEnv) -> Option<RegisteredTool> {
247 let (tool, snippet, guidelines): (Arc<dyn Tool>, &str, &[&str]) = match name {
248 "read" => (
249 Arc::new(Read::new(env.clone())),
250 "Read file contents",
251 &["Use read to examine files instead of cat or sed."],
252 ),
253 "bash" => (
254 Arc::new(Bash::new(env.clone())),
255 "Execute bash commands (ls, grep, find, etc.)",
256 &["You can inspect PI_* environment variables for current model and session details."],
257 ),
258 "edit" => (
259 Arc::new(Edit::new(env.clone())),
260 "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
261 &[
262 "Use edit for precise changes (edits[].oldText must match exactly)",
263 "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
264 "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
265 "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
266 ],
267 ),
268 "write" => (
269 Arc::new(Write::new(env.clone())),
270 "Create or overwrite files",
271 &["Use write only for new files or complete rewrites."],
272 ),
273 "grep" => (
274 Arc::new(Grep::new(env.clone())),
275 "Search file contents for patterns (respects .gitignore)",
276 &[],
277 ),
278 "find" => (
279 Arc::new(Find::new(env.clone())),
280 "Find files by glob pattern (respects .gitignore)",
281 &[],
282 ),
283 "ls" => (
284 Arc::new(Ls::new(env.clone())),
285 "List directory contents",
286 &[],
287 ),
288 _ => return None,
289 };
290 Some(RegisteredTool::direct(
291 tool,
292 Some(snippet.to_owned()),
293 guidelines.iter().map(|g| (*g).to_owned()).collect(),
294 ))
295}
296
297fn declaration(name: &str, description: String, parameters: Value) -> ToolDeclaration {
300 ToolDeclaration {
301 constrained_sampling: Some(serde_json::json!({"type": "json_schema", "strict": "prefer"})),
302 ..plain_declaration(name, description, parameters)
303 }
304}
305
306fn plain_declaration(name: &str, description: String, parameters: Value) -> ToolDeclaration {
308 ToolDeclaration {
309 name: name.to_owned(),
310 description,
311 parameters,
312 constrained_sampling: None,
313 }
314}
315
316fn check_abort(cancel: &CancellationToken) -> Result<(), String> {
318 if cancel.is_cancelled() {
319 Err("Operation aborted".to_owned())
320 } else {
321 Ok(())
322 }
323}
324
325pub(crate) async fn sleep_until_opt(deadline: Option<std::time::Instant>) {
327 match deadline {
328 Some(deadline) => tokio::time::sleep_until(deadline.into()).await,
329 None => std::future::pending().await,
330 }
331}
332
333fn string_arg<'a>(args: &'a Value, name: &str) -> Result<&'a str, String> {
337 args[name]
338 .as_str()
339 .ok_or_else(|| format!("The \"{name}\" argument must be of type string."))
340}
341
342fn text_result(content: impl Into<String>, details: Option<Value>) -> ToolResult {
343 ToolResult {
344 content: vec![ContentBlock::text(content)],
345 details,
346 ..ToolResult::default()
347 }
348}
349
350pub use yapi_types::js::node_error;
351
352pub(crate) fn error_code(err: &std::io::Error) -> String {
354 match yapi_types::js::errno(err.kind()) {
355 Some((code, _)) => format!("Error code: {code}"),
356 None => err.to_string(),
357 }
358}
359
360pub fn js_number(value: f64) -> String {
362 yapi_types::json::to_string(&value).unwrap_or_default()
363}
364
365fn capped_output(
369 raw: &str,
370 before: Vec<String>,
371 after: Vec<String>,
372 mut details: serde_json::Map<String, Value>,
373) -> ToolResult {
374 let mut notices = before;
375 let truncation = truncate::truncate_head(raw, JS_MAX_SAFE_INTEGER, truncate::DEFAULT_MAX_BYTES);
376 let mut output = truncation.content.clone();
377 if truncation.truncated {
378 notices.push(format!(
379 "{} limit reached",
380 truncate::format_size(truncate::DEFAULT_MAX_BYTES)
381 ));
382 details.insert(
383 "truncation".into(),
384 serde_json::to_value(&truncation).unwrap_or_default(),
385 );
386 }
387 notices.extend(after);
388 if !notices.is_empty() {
389 output += &format!("\n\n[{}]", notices.join(". "));
390 }
391 text_result(
392 output,
393 (!details.is_empty()).then_some(Value::Object(details)),
394 )
395}
396
397const JS_MAX_SAFE_INTEGER: usize = 9_007_199_254_740_991;