1use crate::{ProtocolRequestProfile, agent_response_json_schema_json};
4
5const PROTOCOL_INSTRUCTIONS_MARKER: &str = "Structured response protocol:";
6const PROTOCOL_REFRESH_REMINDER_MARKER: &str = "Protocol refresh reminder:";
7const REPAIR_RESPONSE_PREVIEW_MAX_CHARS: usize = 500;
8
9const PROTOCOL_INSTRUCTION_PROMPT_TEMPLATE: &str =
10 include_str!("template/protocol_instruction_prompt.md");
11const PROTOCOL_INSTRUCTION_POLICY_PROMPT_TEMPLATE: &str =
12 include_str!("template/protocol_instruction_policy_prompt.md");
13const PROTOCOL_INSTRUCTION_SESSION_TURN_USAGE_TEMPLATE: &str =
14 include_str!("template/protocol_instruction_session_turn_usage.md");
15const PROTOCOL_INSTRUCTION_UTILITY_PROMPT_USAGE_TEMPLATE: &str =
16 include_str!("template/protocol_instruction_utility_prompt_usage.md");
17const PROTOCOL_REFRESH_PROMPT_TEMPLATE: &str = include_str!("template/protocol_refresh_prompt.md");
18const PROTOCOL_REFRESH_SESSION_TURN_INSTRUCTION_TEMPLATE: &str =
19 include_str!("template/protocol_refresh_session_turn_instruction.md");
20const PROTOCOL_REFRESH_UTILITY_PROMPT_INSTRUCTION_TEMPLATE: &str =
21 include_str!("template/protocol_refresh_utility_prompt_instruction.md");
22const PROTOCOL_REPAIR_PROMPT_TEMPLATE: &str = include_str!("template/protocol_repair_prompt.md");
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ProtocolSchemaInstructionMode {
28 PromptSchema,
31 TransportSchema,
34}
35
36impl ProtocolSchemaInstructionMode {
37 fn includes_response_json_schema(self) -> bool {
40 matches!(self, Self::PromptSchema)
41 }
42}
43
44#[must_use]
54pub fn prepend_protocol_instructions(
55 prompt: &str,
56 profile: ProtocolRequestProfile,
57 schema_instruction_mode: ProtocolSchemaInstructionMode,
58) -> String {
59 if prompt.contains(PROTOCOL_INSTRUCTIONS_MARKER) {
60 return prompt.to_string();
61 }
62
63 let protocol_usage_instructions = render_protocol_usage_instructions(profile);
64 if !schema_instruction_mode.includes_response_json_schema() {
65 return render_template(
66 PROTOCOL_INSTRUCTION_POLICY_PROMPT_TEMPLATE,
67 &[
68 (
69 "{{ protocol_usage_instructions }}",
70 &protocol_usage_instructions,
71 ),
72 ("{{ prompt }}", prompt),
73 ],
74 );
75 }
76
77 let response_json_schema = agent_response_json_schema_json();
78
79 render_template(
80 PROTOCOL_INSTRUCTION_PROMPT_TEMPLATE,
81 &[
82 (
83 "{{ protocol_usage_instructions }}",
84 &protocol_usage_instructions,
85 ),
86 ("{{ response_json_schema }}", &response_json_schema),
87 ("{{ prompt }}", prompt),
88 ],
89 )
90}
91
92#[must_use]
95pub fn prepend_protocol_refresh_reminder(prompt: &str, profile: ProtocolRequestProfile) -> String {
96 if prompt.contains(PROTOCOL_INSTRUCTIONS_MARKER)
97 || prompt.contains(PROTOCOL_REFRESH_REMINDER_MARKER)
98 {
99 return prompt.to_string();
100 }
101
102 let protocol_refresh_instructions = render_protocol_refresh_instructions(profile);
103
104 render_template(
105 PROTOCOL_REFRESH_PROMPT_TEMPLATE,
106 &[
107 (
108 "{{ protocol_refresh_instructions }}",
109 &protocol_refresh_instructions,
110 ),
111 ("{{ prompt }}", prompt),
112 ],
113 )
114}
115
116#[must_use]
122pub fn build_protocol_repair_prompt(parse_error: &str, malformed_response: &str) -> String {
123 let response_json_schema = agent_response_json_schema_json();
124 let response_preview = truncate_preview(malformed_response, REPAIR_RESPONSE_PREVIEW_MAX_CHARS);
125
126 render_template(
127 PROTOCOL_REPAIR_PROMPT_TEMPLATE,
128 &[
129 ("{{ parse_error }}", parse_error),
130 ("{{ response_json_schema }}", &response_json_schema),
131 ("{{ response_preview }}", &response_preview),
132 ],
133 )
134}
135
136fn render_protocol_usage_instructions(profile: ProtocolRequestProfile) -> String {
137 if matches!(profile, ProtocolRequestProfile::SessionTurn) {
138 return trim_template(PROTOCOL_INSTRUCTION_SESSION_TURN_USAGE_TEMPLATE);
139 }
140
141 trim_template(PROTOCOL_INSTRUCTION_UTILITY_PROMPT_USAGE_TEMPLATE)
142}
143
144fn render_protocol_refresh_instructions(profile: ProtocolRequestProfile) -> String {
145 if matches!(profile, ProtocolRequestProfile::SessionTurn) {
146 return trim_template(PROTOCOL_REFRESH_SESSION_TURN_INSTRUCTION_TEMPLATE);
147 }
148
149 trim_template(PROTOCOL_REFRESH_UTILITY_PROMPT_INSTRUCTION_TEMPLATE)
150}
151
152fn render_template(template: &str, replacements: &[(&str, &str)]) -> String {
153 let mut rendered = template.to_string();
154 for (placeholder, value) in replacements {
155 rendered = rendered.replace(placeholder, value);
156 }
157
158 trim_template(&rendered)
159}
160
161fn trim_template(template: &str) -> String {
162 template.trim_end().to_string()
163}
164
165fn truncate_preview(raw: &str, max_chars: usize) -> String {
166 let preview: String = raw.chars().take(max_chars).collect();
167 let total_chars = raw.chars().count();
168
169 if total_chars <= max_chars {
170 return preview;
171 }
172
173 format!("{preview}\n... [{} more chars]", total_chars - max_chars)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_prepend_protocol_instructions_adds_session_protocol_instructions() {
183 let prompt = "Implement feature";
185
186 let rendered_prompt = prepend_protocol_instructions(
188 prompt,
189 ProtocolRequestProfile::SessionTurn,
190 ProtocolSchemaInstructionMode::PromptSchema,
191 );
192
193 assert!(rendered_prompt.contains("File path output requirements:"));
195 assert!(rendered_prompt.contains("repository-root-relative POSIX paths"));
196 assert!(
197 rendered_prompt.contains("Allowed forms: `path`, `path:line`, `path:line:column`.")
198 );
199 assert!(rendered_prompt.contains("If you run git commands, use read-only commands only"));
200 assert!(rendered_prompt.contains("Do not run mutating git commands"));
201 assert!(rendered_prompt.contains("Quality check requirements:"));
202 assert!(rendered_prompt.contains("repository-defined quality checks"));
203 let normalized_rendered_prompt = rendered_prompt.split_whitespace().collect::<Vec<_>>();
204 let normalized_rendered_prompt = normalized_rendered_prompt.join(" ");
205 assert!(normalized_rendered_prompt.contains("affected dependencies and dependents"));
206 assert!(rendered_prompt.contains("full repository test/check suite"));
207 assert!(rendered_prompt.contains("Remove any temporary scripts or files"));
208 assert!(rendered_prompt.contains("Structured response protocol:"));
209 assert!(rendered_prompt.contains("Return a single JSON object"));
210 assert!(rendered_prompt.contains("Do not wrap the JSON in markdown code fences."));
211 assert!(rendered_prompt.contains("Follow this JSON Schema exactly."));
212 assert!(rendered_prompt.contains("Treat the JSON Schema titles and descriptions"));
213 assert!(rendered_prompt.contains("Authoritative JSON Schema:"));
214 assert!(
215 rendered_prompt
216 .contains("______________________________________________________________________")
217 );
218 assert!(!rendered_prompt.contains("{# task separator #}"));
219 assert!(rendered_prompt.contains("For this session turn"));
220 assert!(normalized_rendered_prompt.contains("Do not create commits"));
221 assert!(normalized_rendered_prompt.contains("suggest creating commits"));
222 assert!(rendered_prompt.contains("summary"));
223 assert!(rendered_prompt.contains("turn"));
224 assert!(rendered_prompt.contains("session"));
225 assert!(rendered_prompt.contains("\"answer\""));
226 assert!(rendered_prompt.contains("\"questions\""));
227 assert!(rendered_prompt.contains("\"title\""));
228 assert!(rendered_prompt.contains("\"description\""));
229 assert!(rendered_prompt.contains("summary"));
230 assert!(rendered_prompt.ends_with(prompt));
231 }
232
233 #[test]
234 fn test_prepend_protocol_instructions_omits_schema_for_transport_schema_mode() {
237 let prompt = "Implement feature";
239
240 let rendered_prompt = prepend_protocol_instructions(
242 prompt,
243 ProtocolRequestProfile::SessionTurn,
244 ProtocolSchemaInstructionMode::TransportSchema,
245 );
246
247 assert!(rendered_prompt.contains("Structured response protocol:"));
249 assert!(rendered_prompt.contains("provider enforces Agentty's response JSON schema"));
250 assert!(rendered_prompt.contains("Return a single JSON object"));
251 assert!(!rendered_prompt.contains("Follow this JSON Schema exactly."));
252 assert!(!rendered_prompt.contains("Authoritative JSON Schema:"));
253 assert!(rendered_prompt.ends_with(prompt));
254 }
255
256 #[test]
257 fn test_prepend_protocol_instructions_is_idempotent() {
259 let prompt = prepend_protocol_instructions(
261 "Implement feature",
262 ProtocolRequestProfile::SessionTurn,
263 ProtocolSchemaInstructionMode::PromptSchema,
264 );
265
266 let rendered_prompt = prepend_protocol_instructions(
268 &prompt,
269 ProtocolRequestProfile::UtilityPrompt,
270 ProtocolSchemaInstructionMode::TransportSchema,
271 );
272
273 assert_eq!(rendered_prompt, prompt);
275 }
276
277 #[test]
278 fn test_prepend_protocol_instructions_reuses_same_contract_for_one_shot() {
281 let prompt = "Generate title";
283
284 let rendered_prompt = prepend_protocol_instructions(
286 prompt,
287 ProtocolRequestProfile::UtilityPrompt,
288 ProtocolSchemaInstructionMode::PromptSchema,
289 );
290
291 assert!(rendered_prompt.contains("Structured response protocol:"));
293 assert!(
294 rendered_prompt
295 .contains("______________________________________________________________________")
296 );
297 assert!(rendered_prompt.contains("For this one-shot utility prompt"));
298 assert!(rendered_prompt.contains(r#"{"answer":"...","questions":[],"summary":null}"#));
299 assert!(rendered_prompt.contains("\"summary\""));
300 assert!(rendered_prompt.ends_with(prompt));
301 }
302
303 #[test]
304 fn test_prepend_protocol_instructions_preserves_prompt_placeholders() {
307 let prompt =
309 "Keep these literal: {{ response_json_schema }} {{ protocol_usage_instructions }}";
310
311 let rendered_prompt = prepend_protocol_instructions(
313 prompt,
314 ProtocolRequestProfile::UtilityPrompt,
315 ProtocolSchemaInstructionMode::PromptSchema,
316 );
317
318 assert!(rendered_prompt.ends_with(prompt));
320 }
321
322 #[test]
323 fn test_prepend_protocol_refresh_reminder_adds_compact_contract_notice() {
326 let prompt = "Continue the implementation";
328
329 let rendered_prompt =
331 prepend_protocol_refresh_reminder(prompt, ProtocolRequestProfile::SessionTurn);
332
333 assert!(rendered_prompt.contains("Protocol refresh reminder:"));
335 assert!(rendered_prompt.contains("repository-root-relative POSIX paths"));
336 assert!(rendered_prompt.contains("If you run git commands, use read-only commands only."));
337 assert!(rendered_prompt.contains("Do not run mutating git commands."));
338 assert!(
339 rendered_prompt
340 .contains("______________________________________________________________________")
341 );
342 assert!(!rendered_prompt.contains("Authoritative JSON Schema:"));
343 assert!(rendered_prompt.ends_with(prompt));
344 }
345
346 #[test]
347 fn test_prepend_protocol_refresh_reminder_preserves_prompt_placeholders() {
350 let prompt = "Keep this literal: {{ protocol_refresh_instructions }}";
352
353 let rendered_prompt =
355 prepend_protocol_refresh_reminder(prompt, ProtocolRequestProfile::SessionTurn);
356
357 assert!(rendered_prompt.ends_with(prompt));
359 }
360
361 #[test]
362 fn test_build_protocol_repair_prompt_includes_error_and_preview() {
364 let parse_error = "response is not valid protocol JSON: invalid JSON";
366 let malformed_response = "plain text response";
367
368 let repair_prompt = build_protocol_repair_prompt(parse_error, malformed_response);
370
371 assert!(repair_prompt.contains(parse_error));
373 assert!(repair_prompt.contains("plain text response"));
374 assert!(repair_prompt.contains("Structured response protocol:"));
375 assert!(repair_prompt.contains("Authoritative JSON Schema:"));
376 assert!(repair_prompt.contains("\"answer\""));
377 }
378
379 #[test]
380 fn test_build_protocol_repair_prompt_preserves_response_preview_placeholders() {
383 let malformed_response = "Keep this literal: {{ response_json_schema }}";
385
386 let repair_prompt =
388 build_protocol_repair_prompt("schema validation failed", malformed_response);
389
390 assert!(repair_prompt.contains(malformed_response));
392 }
393
394 #[test]
395 fn test_build_protocol_repair_prompt_truncates_long_response() {
397 let parse_error = "schema validation failed";
399 let malformed_response = "x".repeat(1000);
400
401 let repair_prompt = build_protocol_repair_prompt(parse_error, &malformed_response);
403
404 assert!(repair_prompt.contains("500 more chars"));
406 assert!(!repair_prompt.contains(&malformed_response));
407 }
408
409 #[test]
410 fn test_build_protocol_repair_prompt_contains_protocol_marker() {
412 let repair_prompt = build_protocol_repair_prompt("error", "response");
414
415 assert!(repair_prompt.contains("Structured response protocol:"));
417 }
418
419 #[test]
420 fn test_truncate_preview_keeps_short_responses_intact() {
422 let preview = truncate_preview("short", 500);
424
425 assert_eq!(preview, "short");
427 }
428
429 #[test]
430 fn test_truncate_preview_truncates_long_responses() {
432 let long_response = "a".repeat(600);
434
435 let preview = truncate_preview(&long_response, 500);
437
438 assert!(preview.starts_with(&"a".repeat(500)));
440 assert!(preview.contains("100 more chars"));
441 }
442}