use askama::Template;
use super::backend::AgentBackendError;
use super::instruction::InstructionDeliveryMode;
use super::protocol::{self, ProtocolRequestProfile};
const PROTOCOL_INSTRUCTIONS_MARKER: &str = "Structured response protocol:";
const PROTOCOL_REFRESH_REMINDER_MARKER: &str = "Protocol refresh reminder:";
#[derive(Template)]
#[template(path = "resume_with_session_output_prompt.md", escape = "none")]
struct ResumeWithSessionOutputPromptTemplate<'a> {
prompt: &'a str,
session_output: &'a str,
}
#[derive(Template)]
#[template(path = "protocol_instruction_prompt.md", escape = "none")]
struct ProtocolInstructionPromptTemplate<'a> {
protocol_usage_instructions: &'a str,
prompt: &'a str,
response_json_schema: &'a str,
}
#[derive(Template)]
#[template(path = "protocol_refresh_prompt.md", escape = "none")]
struct ProtocolRefreshPromptTemplate<'a> {
protocol_refresh_instructions: &'a str,
prompt: &'a str,
}
#[derive(Template)]
#[template(path = "protocol_instruction_session_turn_usage.md", escape = "none")]
struct SessionTurnProtocolUsageInstructionsTemplate;
#[derive(Template)]
#[template(path = "protocol_instruction_utility_prompt_usage.md", escape = "none")]
struct UtilityPromptProtocolUsageInstructionsTemplate;
#[derive(Template)]
#[template(path = "protocol_refresh_session_turn_instruction.md", escape = "none")]
struct SessionTurnProtocolRefreshInstructionsTemplate;
#[derive(Template)]
#[template(
path = "protocol_refresh_utility_prompt_instruction.md",
escape = "none"
)]
struct UtilityPromptProtocolRefreshInstructionsTemplate;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PromptPreparationRequest<'a> {
pub instruction_delivery_mode: InstructionDeliveryMode,
pub prompt: &'a str,
pub protocol_profile: ProtocolRequestProfile,
pub replay_session_output: Option<&'a str>,
}
pub(crate) fn prepare_prompt_text(
request: PromptPreparationRequest<'_>,
) -> Result<String, AgentBackendError> {
match request.instruction_delivery_mode {
InstructionDeliveryMode::BootstrapFull => {
prepend_protocol_instructions(request.prompt, request.protocol_profile)
}
InstructionDeliveryMode::DeltaOnly => {
prepend_protocol_refresh_reminder(request.prompt, request.protocol_profile)
}
InstructionDeliveryMode::BootstrapWithReplay => {
let prompt = build_resume_prompt(request.prompt, request.replay_session_output)?;
prepend_protocol_instructions(&prompt, request.protocol_profile)
}
}
}
pub(crate) fn build_resume_prompt(
prompt: &str,
session_output: Option<&str>,
) -> Result<String, AgentBackendError> {
let Some(session_output) = session_output
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(prompt.to_string());
};
let template = ResumeWithSessionOutputPromptTemplate {
prompt,
session_output,
};
render_template("resume_with_session_output_prompt.md", &template)
}
pub(crate) fn prepend_protocol_instructions(
prompt: &str,
profile: ProtocolRequestProfile,
) -> Result<String, AgentBackendError> {
if prompt.contains(PROTOCOL_INSTRUCTIONS_MARKER) {
return Ok(prompt.to_string());
}
let response_json_schema = protocol::agent_response_json_schema_json();
let protocol_usage_instructions = render_protocol_usage_instructions(profile)?;
let template = ProtocolInstructionPromptTemplate {
protocol_usage_instructions: &protocol_usage_instructions,
prompt,
response_json_schema: &response_json_schema,
};
render_template("protocol_instruction_prompt.md", &template)
}
pub(crate) fn prepend_protocol_refresh_reminder(
prompt: &str,
profile: ProtocolRequestProfile,
) -> Result<String, AgentBackendError> {
if prompt.contains(PROTOCOL_INSTRUCTIONS_MARKER)
|| prompt.contains(PROTOCOL_REFRESH_REMINDER_MARKER)
{
return Ok(prompt.to_string());
}
let protocol_refresh_instructions = render_protocol_refresh_instructions(profile)?;
let template = ProtocolRefreshPromptTemplate {
protocol_refresh_instructions: &protocol_refresh_instructions,
prompt,
};
render_template("protocol_refresh_prompt.md", &template)
}
fn render_protocol_usage_instructions(
profile: ProtocolRequestProfile,
) -> Result<String, AgentBackendError> {
match profile {
ProtocolRequestProfile::SessionTurn => render_template(
"protocol_instruction_session_turn_usage.md",
&SessionTurnProtocolUsageInstructionsTemplate,
),
ProtocolRequestProfile::UtilityPrompt => render_template(
"protocol_instruction_utility_prompt_usage.md",
&UtilityPromptProtocolUsageInstructionsTemplate,
),
}
}
fn render_protocol_refresh_instructions(
profile: ProtocolRequestProfile,
) -> Result<String, AgentBackendError> {
match profile {
ProtocolRequestProfile::SessionTurn => render_template(
"protocol_refresh_session_turn_instruction.md",
&SessionTurnProtocolRefreshInstructionsTemplate,
),
ProtocolRequestProfile::UtilityPrompt => render_template(
"protocol_refresh_utility_prompt_instruction.md",
&UtilityPromptProtocolRefreshInstructionsTemplate,
),
}
}
fn render_template(
template_name: &str,
template: &impl Template,
) -> Result<String, AgentBackendError> {
let rendered = template.render().map_err(|error| {
AgentBackendError::CommandBuild(format!("Failed to render `{template_name}`: {error}"))
})?;
Ok(rendered.trim_end().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_resume_prompt_includes_session_output_and_prompt() {
let prompt = "Continue and update tests";
let session_output = Some(" previous output line \n");
let resume_prompt =
build_resume_prompt(prompt, session_output).expect("resume prompt should render");
assert!(resume_prompt.contains("previous output line"));
assert!(resume_prompt.contains("Continue and update tests"));
}
#[test]
fn test_build_resume_prompt_returns_original_prompt_when_output_is_blank() {
let prompt = "Follow-up request";
let session_output = Some(" ");
let resume_prompt =
build_resume_prompt(prompt, session_output).expect("resume prompt should render");
assert_eq!(resume_prompt, prompt);
}
#[test]
fn test_build_resume_prompt_returns_original_prompt_without_output() {
let prompt = "Retry merge";
let resume_prompt = build_resume_prompt(prompt, None).expect("resume prompt should render");
assert_eq!(resume_prompt, prompt);
}
#[test]
fn test_prepend_protocol_instructions_adds_session_protocol_instructions() {
let prompt = "Implement feature";
let rendered_prompt =
prepend_protocol_instructions(prompt, ProtocolRequestProfile::SessionTurn)
.expect("protocol instruction prompt should render");
assert!(rendered_prompt.contains("File path output requirements:"));
assert!(rendered_prompt.contains("repository-root-relative POSIX paths"));
assert!(rendered_prompt.contains("Paths must be relative to the repository root."));
assert!(rendered_prompt.contains("If you run git commands, use read-only commands only"));
assert!(rendered_prompt.contains("Do not run mutating git commands"));
assert!(rendered_prompt.contains("Quality check requirements:"));
assert!(rendered_prompt.contains("repository-defined quality checks"));
assert!(rendered_prompt.contains("affected dependencies and dependents"));
assert!(rendered_prompt.contains("full repository test/check suite"));
assert!(rendered_prompt.contains("Structured response protocol:"));
assert!(rendered_prompt.contains("Return a single JSON object"));
assert!(rendered_prompt.contains("Do not wrap the JSON in markdown code fences."));
assert!(rendered_prompt.contains("Follow this JSON Schema exactly."));
assert!(rendered_prompt.contains("Treat the JSON Schema titles and descriptions"));
assert!(rendered_prompt.contains("Authoritative JSON Schema:"));
assert!(rendered_prompt.contains("---"));
assert!(rendered_prompt.contains("For this session turn"));
assert!(rendered_prompt.contains("summary"));
assert!(rendered_prompt.contains("turn"));
assert!(rendered_prompt.contains("session"));
assert!(rendered_prompt.contains("\"answer\""));
assert!(rendered_prompt.contains("\"questions\""));
assert!(rendered_prompt.contains("\"follow_up_tasks\""));
assert!(rendered_prompt.contains("\"title\""));
assert!(rendered_prompt.contains("\"description\""));
assert!(rendered_prompt.contains("summary"));
assert!(rendered_prompt.contains("changes code or tests"));
assert!(rendered_prompt.contains("not standalone command execution"));
assert!(rendered_prompt.ends_with(prompt));
}
#[test]
fn test_prepend_protocol_instructions_is_idempotent() {
let prompt =
prepend_protocol_instructions("Implement feature", ProtocolRequestProfile::SessionTurn)
.expect("protocol instruction prompt should render");
let rendered_prompt =
prepend_protocol_instructions(&prompt, ProtocolRequestProfile::UtilityPrompt)
.expect("protocol instruction prompt should render");
assert_eq!(rendered_prompt, prompt);
}
#[test]
fn test_prepend_protocol_instructions_reuses_same_contract_for_one_shot() {
let prompt = "Generate title";
let rendered_prompt =
prepend_protocol_instructions(prompt, ProtocolRequestProfile::UtilityPrompt)
.expect("protocol instruction prompt should render");
assert!(rendered_prompt.contains("Structured response protocol:"));
assert!(rendered_prompt.contains("---"));
assert!(rendered_prompt.contains("For this one-shot utility prompt"));
assert!(
rendered_prompt
.contains(r#"{"answer":"...","questions":[],"follow_up_tasks":[],"summary":null}"#)
);
assert!(rendered_prompt.contains("\"follow_up_tasks\""));
assert!(rendered_prompt.contains("\"summary\""));
assert!(rendered_prompt.ends_with(prompt));
}
#[test]
fn test_prepare_prompt_text_applies_replay_and_protocol_instructions() {
let request = PromptPreparationRequest {
instruction_delivery_mode: InstructionDeliveryMode::BootstrapWithReplay,
prompt: "Continue edits",
protocol_profile: ProtocolRequestProfile::SessionTurn,
replay_session_output: Some("previous output"),
};
let prepared_prompt = prepare_prompt_text(request).expect("prompt should render");
assert!(prepared_prompt.contains("Structured response protocol:"));
assert!(prepared_prompt.contains("previous output"));
assert!(prepared_prompt.ends_with("Continue edits"));
}
#[test]
fn test_prepend_protocol_refresh_reminder_adds_compact_contract_notice() {
let prompt = "Continue the implementation";
let rendered_prompt =
prepend_protocol_refresh_reminder(prompt, ProtocolRequestProfile::SessionTurn)
.expect("protocol refresh reminder should render");
assert!(rendered_prompt.contains("Protocol refresh reminder:"));
assert!(rendered_prompt.contains("repository-root-relative POSIX paths"));
assert!(rendered_prompt.contains("read-only git commands"));
assert!(rendered_prompt.contains("follow_up_tasks"));
assert!(!rendered_prompt.contains("Authoritative JSON Schema:"));
assert!(rendered_prompt.ends_with(prompt));
}
#[test]
fn test_prepare_prompt_text_uses_delta_only_refresh_mode() {
let request = PromptPreparationRequest {
instruction_delivery_mode: InstructionDeliveryMode::DeltaOnly,
prompt: "Continue edits",
protocol_profile: ProtocolRequestProfile::SessionTurn,
replay_session_output: Some("previous output"),
};
let prepared_prompt = prepare_prompt_text(request).expect("prompt should render");
assert!(prepared_prompt.contains("Protocol refresh reminder:"));
assert!(!prepared_prompt.contains("Authoritative JSON Schema:"));
assert!(!prepared_prompt.contains("previous output"));
assert!(prepared_prompt.ends_with("Continue edits"));
}
}