1use std::path::Path;
5
6use serde_json::{Value, json};
7
8use crate::actions::Action;
9use crate::conversation::Conversation;
10
11pub const AGENTS_FILE: &str = "AGENTS.md";
13
14const AGENTS_LIMIT: usize = 64 * 1024;
17
18const MAX_TOKENS: u32 = 16_384;
20
21pub const REQUEST_CAP: usize = 256 * 1024;
29
30#[must_use]
32pub fn system(root: &Path) -> String {
33 let mut text = format!(
34 "You are qcli, a coding agent in a terminal. You work in the folder {}. \
35 Use the tools to look at the files before you answer questions about them: \
36 list_dir to see the shape of a folder and how big its files are, read_file the file you \
37 need, search to find where something is. \
38 In a file longer than 2000 lines read the part you need with start_line and end_line \
39 rather than asking for the whole of it, and read on with the lines the reply says are \
40 not shown. \
41 When a search finds nothing, try again with ignore_case or with a glob such as *.rs \
42 rather than sending the same search again. \
43 Paths are relative to that folder, and nothing outside it can be reached. \
44 Every edit and every command is shown to the person first, and they may decline it; \
45 when they do, do not try the same thing another way, ask what they want instead. \
46 Keep answers short and plain.",
47 root.display()
48 );
49 if let Some(agents) = agents(root) {
50 text.push_str("\n\nThe folder's AGENTS.md says:\n\n");
51 text.push_str(&agents);
52 }
53 text
54}
55
56#[must_use]
58pub fn agents(root: &Path) -> Option<String> {
59 let text = std::fs::read_to_string(root.join(AGENTS_FILE)).ok()?;
60 let text = text.trim();
61 if text.is_empty() {
62 return None;
63 }
64 Some(match text.char_indices().nth(AGENTS_LIMIT) {
65 Some((end, _)) => format!("{}\n\n(The rest of the file was left out: it is too long.)", &text[..end]),
66 None => text.to_owned(),
67 })
68}
69
70#[must_use]
73pub fn body(conversation: &Conversation, system: &str) -> Value {
74 body_within(conversation, system, REQUEST_CAP)
75}
76
77#[must_use]
82pub fn body_within(conversation: &Conversation, system: &str, cap: usize) -> Value {
83 let tools = Action::definitions();
84 let rest = serde_json::to_string(&json!({
87 "max_tokens": MAX_TOKENS,
88 "system": system,
89 "messages": [],
90 "tools": tools,
91 }))
92 .map_or(0, |body| body.len())
93 .saturating_sub(2);
94 json!({
95 "max_tokens": MAX_TOKENS,
96 "system": system,
97 "messages": conversation.view(cap.saturating_sub(rest)),
98 "tools": tools,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn folder(test: &str) -> std::path::PathBuf {
109 let root = std::env::temp_dir().join(format!("qcli-instructions-{test}-{}", std::process::id()));
110 let _ = std::fs::remove_dir_all(&root);
111 std::fs::create_dir_all(&root).expect("folder");
112 root
113 }
114
115 #[test]
116 fn the_folders_agents_file_joins_the_system_prompt() {
117 let root = folder("agents");
118 assert!(!system(&root).contains("AGENTS.md says"), "no file, no section");
119 std::fs::write(root.join(AGENTS_FILE), "Always answer in haiku.\n").expect("write");
120 let prompt = system(&root);
121 assert!(prompt.contains(&root.display().to_string()));
122 assert!(prompt.ends_with("Always answer in haiku."), "{prompt}");
123 std::fs::write(root.join(AGENTS_FILE), "x".repeat(AGENTS_LIMIT + 10)).expect("write");
124 assert!(agents(&root).expect("text").ends_with("it is too long.)"));
125 std::fs::remove_dir_all(&root).expect("clean up");
126 }
127
128 #[test]
129 fn the_body_carries_the_conversation_and_the_five_tools() {
130 let mut conversation = Conversation::default();
131 conversation.say("hi");
132 let body = body(&conversation, "be brief");
133 assert_eq!(body["system"], "be brief");
134 assert_eq!(body["messages"][0]["content"][0]["text"], "hi");
135 let names: Vec<_> = body["tools"].as_array().expect("tools").iter().map(|tool| tool["name"].clone()).collect();
136 assert_eq!(names, ["read_file", "list_dir", "search", "edit_file", "run_command"]);
137 }
138
139 #[test]
140 fn the_system_prompt_says_how_to_use_the_new_fields() {
141 let root = folder("fields");
142 let prompt = system(&root);
143 for said in [
144 "list_dir",
145 "start_line",
146 "end_line",
147 "ignore_case",
148 "glob",
149 "*.rs",
150 "do not try the same thing another way",
151 ] {
152 assert!(prompt.contains(said), "`{said}` is not in the prompt:\n{prompt}");
153 }
154 std::fs::remove_dir_all(&root).expect("clean up");
155 }
156}