1use std::path::PathBuf;
8
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11
12use crate::rpc::SourceInfo;
13
14#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(default, rename_all = "camelCase")]
18pub struct SystemPromptOptions {
19 #[serde(skip_serializing_if = "Option::is_none")]
22 pub custom_prompt: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub force_system_prompt: Option<String>,
26 pub selected_tools: Vec<String>,
28 pub tool_snippets: IndexMap<String, String>,
30 pub tool_guidelines: IndexMap<String, Vec<String>>,
32 pub prompt_guidelines: Vec<String>,
34 pub append_system_prompt: String,
37 pub sections: IndexMap<String, String>,
39 pub cwd: PathBuf,
41 pub context_files: Vec<ContextFile>,
43 pub skills: Vec<Skill>,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ContextFile {
50 pub path: PathBuf,
52 pub content: String,
54}
55
56#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(default, rename_all = "camelCase")]
60pub struct Skill {
61 pub name: String,
63 pub description: String,
65 pub file_path: PathBuf,
67 pub base_dir: PathBuf,
69 #[serde(rename = "sourceInfo")]
71 pub source: SourceInfo,
72 pub disable_model_invocation: bool,
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
83 fn serializes_in_pi_order() {
84 let mut options = SystemPromptOptions {
85 custom_prompt: Some("custom".into()),
86 force_system_prompt: Some("forced".into()),
87 selected_tools: vec!["read".into(), "bash".into()],
88 prompt_guidelines: vec!["g".into()],
89 append_system_prompt: "app".into(),
90 cwd: "/work".into(),
91 context_files: vec![ContextFile {
92 path: "/work/AGENTS.md".into(),
93 content: "c".into(),
94 }],
95 skills: vec![Skill {
96 name: "s".into(),
97 description: "d".into(),
98 file_path: "/s/SKILL.md".into(),
99 base_dir: "/s".into(),
100 source: SourceInfo {
101 path: "/s/SKILL.md".into(),
102 source: "auto".into(),
103 scope: "user".into(),
104 origin: "top-level".into(),
105 base_dir: Some("/s".into()),
106 },
107 disable_model_invocation: true,
108 }],
109 ..SystemPromptOptions::default()
110 };
111 options.tool_snippets.insert("read".into(), "Read".into());
112 options
113 .tool_guidelines
114 .insert("read".into(), vec!["r".into()]);
115 options.sections.insert("notes".into(), "n".into());
116 assert_eq!(
117 crate::json::to_string(&options).unwrap(),
118 r#"{"customPrompt":"custom","forceSystemPrompt":"forced","selectedTools":["read","bash"],"toolSnippets":{"read":"Read"},"toolGuidelines":{"read":["r"]},"promptGuidelines":["g"],"appendSystemPrompt":"app","sections":{"notes":"n"},"cwd":"/work","contextFiles":[{"path":"/work/AGENTS.md","content":"c"}],"skills":[{"name":"s","description":"d","filePath":"/s/SKILL.md","baseDir":"/s","sourceInfo":{"path":"/s/SKILL.md","source":"auto","scope":"user","origin":"top-level","baseDir":"/s"},"disableModelInvocation":true}]}"#
119 );
120 assert_eq!(
121 crate::json::to_string(&SystemPromptOptions::default()).unwrap(),
122 r#"{"selectedTools":[],"toolSnippets":{},"toolGuidelines":{},"promptGuidelines":[],"appendSystemPrompt":"","sections":{},"cwd":"","contextFiles":[],"skills":[]}"#
123 );
124 }
125
126 #[test]
128 fn reads_partial_edits() {
129 let options: SystemPromptOptions = serde_json::from_str(
130 r#"{"selectedTools":["read"],"skills":[{"name":"s","description":"d","filePath":"/s/SKILL.md"}]}"#,
131 )
132 .unwrap();
133 assert_eq!(options.selected_tools, ["read"]);
134 assert_eq!(options.cwd, PathBuf::new());
135 assert_eq!(options.skills[0].name, "s");
136 assert!(!options.skills[0].disable_model_invocation);
137 }
138}