Skip to main content

yapi_types/
system_prompt.rs

1//! pi's system prompt inputs, as `before_agent_start` hands them to extensions.
2//!
3//! Mirrors `NormalizedBuildSystemPromptOptions` in
4//! `packages/coding-agent/src/core/system-prompt.ts` and `Skill` in
5//! `core/skills.ts` in pi `v1.0.0`.
6
7use std::path::PathBuf;
8
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11
12use crate::rpc::SourceInfo;
13
14/// pi's normalized `BuildSystemPromptOptions`. Handlers edit it and hand it
15/// back, so a key they removed takes its empty value.
16#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(default, rename_all = "camelCase")]
18pub struct SystemPromptOptions {
19    /// Replaces the default preamble, tools, rules and docs (`SYSTEM.md`,
20    /// `--system-prompt`).
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub custom_prompt: Option<String>,
23    /// The whole prompt, as a `before_agent_start` handler set it.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub force_system_prompt: Option<String>,
26    /// Active tool names, in order.
27    pub selected_tools: Vec<String>,
28    /// One-line summaries by tool name; tools without one are not listed.
29    pub tool_snippets: IndexMap<String, String>,
30    /// Rule bullets by tool name.
31    pub tool_guidelines: IndexMap<String, Vec<String>>,
32    /// Extra rule bullets.
33    pub prompt_guidelines: Vec<String>,
34    /// Appended after the rules (`APPEND_SYSTEM.md`, `--append-system-prompt`);
35    /// empty for none.
36    pub append_system_prompt: String,
37    /// Extra sections from extensions, by tag name.
38    pub sections: IndexMap<String, String>,
39    /// Working directory.
40    pub cwd: PathBuf,
41    /// Context files.
42    pub context_files: Vec<ContextFile>,
43    /// Skills.
44    pub skills: Vec<Skill>,
45}
46
47/// A context file and its text.
48#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ContextFile {
50    /// Where it was found.
51    pub path: PathBuf,
52    /// Its text, without a byte order mark.
53    pub content: String,
54}
55
56/// A skill: instructions the model reads on demand. A handler may add one
57/// with only the fields the prompt shows; the others take their empty value.
58#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(default, rename_all = "camelCase")]
60pub struct Skill {
61    /// Name from frontmatter, else the directory name.
62    pub name: String,
63    /// When to use it.
64    pub description: String,
65    /// The skill file.
66    pub file_path: PathBuf,
67    /// Its directory, for relative references.
68    pub base_dir: PathBuf,
69    /// Where it was found.
70    #[serde(rename = "sourceInfo")]
71    pub source: SourceInfo,
72    /// Hidden from the prompt; only usable as `/skill:name`.
73    pub disable_model_invocation: bool,
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    /// The options keep the key order of pi's `normalizeBuildSystemPromptOptions`
81    /// and leave out unset optional keys, as `JSON.stringify` does.
82    #[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    /// A handler's edit may drop keys and add a skill with only some fields.
127    #[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}