yapi-types 0.1.0

Serde types for pi's JSON formats: messages, events, session entries, settings, models and extension payloads.
Documentation
//! pi's system prompt inputs, as `before_agent_start` hands them to extensions.
//!
//! Mirrors `NormalizedBuildSystemPromptOptions` in
//! `packages/coding-agent/src/core/system-prompt.ts` and `Skill` in
//! `core/skills.ts` in pi `v1.0.0`.

use std::path::PathBuf;

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

use crate::rpc::SourceInfo;

/// pi's normalized `BuildSystemPromptOptions`. Handlers edit it and hand it
/// back, so a key they removed takes its empty value.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SystemPromptOptions {
    /// Replaces the default preamble, tools, rules and docs (`SYSTEM.md`,
    /// `--system-prompt`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_prompt: Option<String>,
    /// The whole prompt, as a `before_agent_start` handler set it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub force_system_prompt: Option<String>,
    /// Active tool names, in order.
    pub selected_tools: Vec<String>,
    /// One-line summaries by tool name; tools without one are not listed.
    pub tool_snippets: IndexMap<String, String>,
    /// Rule bullets by tool name.
    pub tool_guidelines: IndexMap<String, Vec<String>>,
    /// Extra rule bullets.
    pub prompt_guidelines: Vec<String>,
    /// Appended after the rules (`APPEND_SYSTEM.md`, `--append-system-prompt`);
    /// empty for none.
    pub append_system_prompt: String,
    /// Extra sections from extensions, by tag name.
    pub sections: IndexMap<String, String>,
    /// Working directory.
    pub cwd: PathBuf,
    /// Context files.
    pub context_files: Vec<ContextFile>,
    /// Skills.
    pub skills: Vec<Skill>,
}

/// A context file and its text.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextFile {
    /// Where it was found.
    pub path: PathBuf,
    /// Its text, without a byte order mark.
    pub content: String,
}

/// A skill: instructions the model reads on demand. A handler may add one
/// with only the fields the prompt shows; the others take their empty value.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Skill {
    /// Name from frontmatter, else the directory name.
    pub name: String,
    /// When to use it.
    pub description: String,
    /// The skill file.
    pub file_path: PathBuf,
    /// Its directory, for relative references.
    pub base_dir: PathBuf,
    /// Where it was found.
    #[serde(rename = "sourceInfo")]
    pub source: SourceInfo,
    /// Hidden from the prompt; only usable as `/skill:name`.
    pub disable_model_invocation: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The options keep the key order of pi's `normalizeBuildSystemPromptOptions`
    /// and leave out unset optional keys, as `JSON.stringify` does.
    #[test]
    fn serializes_in_pi_order() {
        let mut options = SystemPromptOptions {
            custom_prompt: Some("custom".into()),
            force_system_prompt: Some("forced".into()),
            selected_tools: vec!["read".into(), "bash".into()],
            prompt_guidelines: vec!["g".into()],
            append_system_prompt: "app".into(),
            cwd: "/work".into(),
            context_files: vec![ContextFile {
                path: "/work/AGENTS.md".into(),
                content: "c".into(),
            }],
            skills: vec![Skill {
                name: "s".into(),
                description: "d".into(),
                file_path: "/s/SKILL.md".into(),
                base_dir: "/s".into(),
                source: SourceInfo {
                    path: "/s/SKILL.md".into(),
                    source: "auto".into(),
                    scope: "user".into(),
                    origin: "top-level".into(),
                    base_dir: Some("/s".into()),
                },
                disable_model_invocation: true,
            }],
            ..SystemPromptOptions::default()
        };
        options.tool_snippets.insert("read".into(), "Read".into());
        options
            .tool_guidelines
            .insert("read".into(), vec!["r".into()]);
        options.sections.insert("notes".into(), "n".into());
        assert_eq!(
            crate::json::to_string(&options).unwrap(),
            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}]}"#
        );
        assert_eq!(
            crate::json::to_string(&SystemPromptOptions::default()).unwrap(),
            r#"{"selectedTools":[],"toolSnippets":{},"toolGuidelines":{},"promptGuidelines":[],"appendSystemPrompt":"","sections":{},"cwd":"","contextFiles":[],"skills":[]}"#
        );
    }

    /// A handler's edit may drop keys and add a skill with only some fields.
    #[test]
    fn reads_partial_edits() {
        let options: SystemPromptOptions = serde_json::from_str(
            r#"{"selectedTools":["read"],"skills":[{"name":"s","description":"d","filePath":"/s/SKILL.md"}]}"#,
        )
        .unwrap();
        assert_eq!(options.selected_tools, ["read"]);
        assert_eq!(options.cwd, PathBuf::new());
        assert_eq!(options.skills[0].name, "s");
        assert!(!options.skills[0].disable_model_invocation);
    }
}