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(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolSchemaInstructionMode {
PromptSchema,
TransportSchema,
}
impl ProtocolSchemaInstructionMode {
fn includes_response_json_schema(self) -> bool {
matches!(self, Self::PromptSchema)
}
}
#[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> {
prompt: &'a str,
protocol_usage_instructions: &'a str,
response_json_schema: &'a str,
}
#[derive(Template)]
#[template(path = "protocol_instruction_policy_prompt.md", escape = "none")]
struct ProtocolInstructionPolicyPromptTemplate<'a> {
prompt: &'a str,
protocol_usage_instructions: &'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 schema_instruction_mode: ProtocolSchemaInstructionMode,
}
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,
request.schema_instruction_mode,
),
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,
request.schema_instruction_mode,
)
}
}
}
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,
schema_instruction_mode: ProtocolSchemaInstructionMode,
) -> Result<String, AgentBackendError> {
if prompt.contains(PROTOCOL_INSTRUCTIONS_MARKER) {
return Ok(prompt.to_string());
}
let protocol_usage_instructions = render_protocol_usage_instructions(profile)?;
if !schema_instruction_mode.includes_response_json_schema() {
let template = ProtocolInstructionPolicyPromptTemplate {
prompt,
protocol_usage_instructions: &protocol_usage_instructions,
};
return render_template("protocol_instruction_policy_prompt.md", &template);
}
let response_json_schema = protocol::agent_response_json_schema_json();
let template = ProtocolInstructionPromptTemplate {
prompt,
protocol_usage_instructions: &protocol_usage_instructions,
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,
),
}
}
pub(crate) fn diff_fence(content: &str) -> String {
let mut max_run = 0usize;
let mut current_run = 0usize;
for character in content.chars() {
if character == '`' {
current_run += 1;
if current_run > max_run {
max_run = current_run;
}
} else {
current_run = 0;
}
}
let fence_length = std::cmp::max(3, max_run + 1);
"`".repeat(fence_length)
}
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_diff_fence_returns_minimum_three_backticks_for_plain_diff() {
let diff = "diff --git a/a.rs b/a.rs\n+fn main() {}\n";
let fence = diff_fence(diff);
assert_eq!(fence, "```");
}
#[test]
fn test_diff_fence_exceeds_longest_backtick_run_in_diff() {
let diff = "+```\nsample\n+```\n";
let fence = diff_fence(diff);
assert_eq!(fence, "````");
}
#[test]
fn test_diff_fence_handles_long_backtick_runs() {
let diff = "prefix `````diff\ncontent\n`````\n";
let fence = diff_fence(diff);
assert_eq!(fence, "``````");
}
#[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,
ProtocolSchemaInstructionMode::PromptSchema,
)
.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"));
let normalized_rendered_prompt = rendered_prompt.split_whitespace().collect::<Vec<_>>();
let normalized_rendered_prompt = normalized_rendered_prompt.join(" ");
assert!(normalized_rendered_prompt.contains("affected dependencies and dependents"));
assert!(rendered_prompt.contains("full repository test/check suite"));
assert!(rendered_prompt.contains("Remove any temporary scripts or files"));
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!(normalized_rendered_prompt.contains("Do not create commits"));
assert!(normalized_rendered_prompt.contains("suggest creating commits"));
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("\"title\""));
assert!(rendered_prompt.contains("\"description\""));
assert!(rendered_prompt.contains("summary"));
assert!(rendered_prompt.ends_with(prompt));
}
#[test]
fn test_prepend_protocol_instructions_omits_schema_for_transport_schema_mode() {
let prompt = "Implement feature";
let rendered_prompt = prepend_protocol_instructions(
prompt,
ProtocolRequestProfile::SessionTurn,
ProtocolSchemaInstructionMode::TransportSchema,
)
.expect("protocol instruction prompt should render");
assert!(rendered_prompt.contains("Structured response protocol:"));
assert!(rendered_prompt.contains("provider enforces Agentty's response JSON schema"));
assert!(rendered_prompt.contains("Return a single JSON object"));
assert!(!rendered_prompt.contains("Follow this JSON Schema exactly."));
assert!(!rendered_prompt.contains("Authoritative JSON Schema:"));
assert!(rendered_prompt.ends_with(prompt));
}
#[test]
fn test_prepend_protocol_instructions_is_idempotent() {
let prompt = prepend_protocol_instructions(
"Implement feature",
ProtocolRequestProfile::SessionTurn,
ProtocolSchemaInstructionMode::PromptSchema,
)
.expect("protocol instruction prompt should render");
let rendered_prompt = prepend_protocol_instructions(
&prompt,
ProtocolRequestProfile::UtilityPrompt,
ProtocolSchemaInstructionMode::TransportSchema,
)
.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,
ProtocolSchemaInstructionMode::PromptSchema,
)
.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":[],"summary":null}"#));
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"),
schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
};
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("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"),
schema_instruction_mode: ProtocolSchemaInstructionMode::PromptSchema,
};
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"));
}
}