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
21#[must_use]
23pub fn system(root: &Path) -> String {
24 let mut text = format!(
25 "You are qcli, a coding agent in a terminal. You work in the folder {}. \
26 Use the tools to look at the files before you answer questions about them. \
27 Paths are relative to that folder, and nothing outside it can be reached. \
28 Every edit and every command is shown to the person first, and they may decline it; \
29 when they do, do not try the same thing another way, ask what they want instead. \
30 Keep answers short and plain.",
31 root.display()
32 );
33 if let Some(agents) = agents(root) {
34 text.push_str("\n\nThe folder's AGENTS.md says:\n\n");
35 text.push_str(&agents);
36 }
37 text
38}
39
40#[must_use]
42pub fn agents(root: &Path) -> Option<String> {
43 let text = std::fs::read_to_string(root.join(AGENTS_FILE)).ok()?;
44 let text = text.trim();
45 if text.is_empty() {
46 return None;
47 }
48 Some(match text.char_indices().nth(AGENTS_LIMIT) {
49 Some((end, _)) => format!("{}\n\n(The rest of the file was left out: it is too long.)", &text[..end]),
50 None => text.to_owned(),
51 })
52}
53
54#[must_use]
56pub fn body(conversation: &Conversation, system: &str) -> Value {
57 json!({
58 "max_tokens": MAX_TOKENS,
59 "system": system,
60 "messages": conversation.messages(),
61 "tools": Action::definitions(),
62 })
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn the_folders_agents_file_joins_the_system_prompt() {
71 let root = std::env::temp_dir().join(format!("qcli-instructions-{}", std::process::id()));
72 std::fs::create_dir_all(&root).expect("folder");
73 assert!(!system(&root).contains("AGENTS.md says"), "no file, no section");
74 std::fs::write(root.join(AGENTS_FILE), "Always answer in haiku.\n").expect("write");
75 let prompt = system(&root);
76 assert!(prompt.contains(&root.display().to_string()));
77 assert!(prompt.ends_with("Always answer in haiku."), "{prompt}");
78 std::fs::write(root.join(AGENTS_FILE), "x".repeat(AGENTS_LIMIT + 10)).expect("write");
79 assert!(agents(&root).expect("text").ends_with("it is too long.)"));
80 std::fs::remove_dir_all(&root).expect("clean up");
81 }
82
83 #[test]
84 fn the_body_carries_the_conversation_and_the_five_tools() {
85 let mut conversation = Conversation::default();
86 conversation.say("hi");
87 let body = body(&conversation, "be brief");
88 assert_eq!(body["system"], "be brief");
89 assert_eq!(body["messages"][0]["content"][0]["text"], "hi");
90 let names: Vec<_> = body["tools"].as_array().expect("tools").iter().map(|tool| tool["name"].clone()).collect();
91 assert_eq!(names, ["read_file", "list_dir", "search", "edit_file", "run_command"]);
92 }
93}