1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct WriteParams {
7 pub path: String,
8 pub content: String,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(deny_unknown_fields)]
13pub struct EditParams {
14 pub path: String,
15 pub old_string: String,
16 pub new_string: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub replace_all: Option<bool>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub dry_run: Option<bool>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub ignore_whitespace: Option<bool>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct EditSpec {
30 pub old_string: String,
31 pub new_string: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub replace_all: Option<bool>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub ignore_whitespace: Option<bool>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct MultiEditParams {
43 pub path: String,
44 pub edits: Vec<EditSpec>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub dry_run: Option<bool>,
47}
48
49#[derive(Debug, Clone, thiserror::Error)]
50pub enum WriteParseError {
51 #[error("{0}")]
52 Message(String),
53}
54
55pub fn safe_parse_write_params(input: &Value) -> Result<WriteParams, WriteParseError> {
56 let parsed: WriteParams = serde_json::from_value(input.clone())
57 .map_err(|e| WriteParseError::Message(e.to_string()))?;
58 if parsed.path.is_empty() {
59 return Err(WriteParseError::Message("path must not be empty".to_string()));
60 }
61 Ok(parsed)
62}
63
64pub fn safe_parse_edit_params(input: &Value) -> Result<EditParams, WriteParseError> {
65 let parsed: EditParams = serde_json::from_value(input.clone())
66 .map_err(|e| WriteParseError::Message(e.to_string()))?;
67 if parsed.path.is_empty() {
68 return Err(WriteParseError::Message("path must not be empty".to_string()));
69 }
70 if parsed.old_string.is_empty() {
71 return Err(WriteParseError::Message(
72 "old_string must not be empty".to_string(),
73 ));
74 }
75 Ok(parsed)
76}
77
78pub fn safe_parse_multi_edit_params(input: &Value) -> Result<MultiEditParams, WriteParseError> {
79 let parsed: MultiEditParams = serde_json::from_value(input.clone())
80 .map_err(|e| WriteParseError::Message(e.to_string()))?;
81 if parsed.path.is_empty() {
82 return Err(WriteParseError::Message("path must not be empty".to_string()));
83 }
84 if parsed.edits.is_empty() {
85 return Err(WriteParseError::Message(
86 "edits must contain at least one edit".to_string(),
87 ));
88 }
89 for (i, e) in parsed.edits.iter().enumerate() {
90 if e.old_string.is_empty() {
91 return Err(WriteParseError::Message(format!(
92 "edits[{}].old_string must not be empty",
93 i
94 )));
95 }
96 }
97 Ok(parsed)
98}
99
100pub const WRITE_TOOL_NAME: &str = "write";
101pub const EDIT_TOOL_NAME: &str = "edit";
102pub const MULTIEDIT_TOOL_NAME: &str = "multi_edit";
106#[deprecated(
109 since = "0.3.0",
110 note = "use MULTIEDIT_TOOL_NAME (\"multi_edit\"); the \"multiedit\" spelling will be removed in a future major release"
111)]
112pub const MULTIEDIT_TOOL_NAME_LEGACY: &str = "multiedit";
113
114pub fn is_multi_edit_tool_name(name: &str) -> bool {
123 #[allow(deprecated)]
124 {
125 name == MULTIEDIT_TOOL_NAME || name == MULTIEDIT_TOOL_NAME_LEGACY
126 }
127}
128
129pub fn normalize_multi_edit_tool_name(name: &str) -> Option<&'static str> {
138 if name == MULTIEDIT_TOOL_NAME {
139 return Some(MULTIEDIT_TOOL_NAME);
140 }
141 #[allow(deprecated)]
142 if name == MULTIEDIT_TOOL_NAME_LEGACY {
143 warn_legacy_multi_edit_tool_name();
144 return Some(MULTIEDIT_TOOL_NAME);
145 }
146 None
147}
148
149pub fn warn_legacy_multi_edit_tool_name() {
153 static ONCE: std::sync::Once = std::sync::Once::new();
154 ONCE.call_once(|| {
155 eprintln!(
156 "[harness-write] DEPRECATION: tool name \"multiedit\" is deprecated; use \"multi_edit\". \
157 The \"multiedit\" spelling will be removed in a future major release."
158 );
159 });
160}
161
162pub const WRITE_TOOL_DESCRIPTION: &str = "Create a new file, or overwrite an existing file.\n\nUsage:\n- New file (path does not exist): call Write directly. No prior Read is required.\n- Existing file: you must Read it first in this session, or Write fails with NOT_READ_THIS_SESSION.\n- Prefer Edit or MultiEdit for targeted changes to existing files.\n- Write is atomic: bytes land via a temporary file + rename.\n- Path must be absolute. If relative, it resolves against the session cwd.";
163
164pub const EDIT_TOOL_DESCRIPTION: &str = "Replace exactly one occurrence of old_string with new_string in a file.\n\nUsage:\n- The file must have been Read first in this session.\n- old_string must match the file content exactly, character for character, including whitespace and indentation.\n- If old_string appears more than once, the call fails with OLD_STRING_NOT_UNIQUE.\n- If old_string does not match, the call fails with OLD_STRING_NOT_FOUND and returns the top fuzzy candidates.\n- Use dry_run: true to preview the unified diff without writing.\n- CRLF is normalized to LF on both sides.";
165
166pub const MULTIEDIT_TOOL_DESCRIPTION: &str = "Apply a sequence of edits to a single file atomically.\n\nUsage:\n- edits is an ordered list of { old_string, new_string, replace_all? } objects.\n- Edits apply sequentially in memory: later edits see the output of earlier edits.\n- If any edit fails, none of the edits are applied and the file is untouched.\n- The file must have been Read first in this session.\n- Use dry_run: true to preview the final unified diff without writing.";