use super::generate_resume_note;
use super::prompt_config::PromptConfig;
use super::prompt_scope_key::PromptScopeKey;
use super::types::{Action, Role};
use super::ContextLevel;
use super::TemplateContext;
use super::{
prompt_developer_iteration_with_context, prompt_fix_with_context, prompt_plan_with_context,
};
use crate::prompts::PromptHistoryEntry;
pub fn prompt_for_agent(
role: Role,
action: Action,
context: ContextLevel,
template_context: &TemplateContext,
config: PromptConfig,
workspace: &dyn crate::workspace::Workspace,
) -> String {
let resume_note = if let Some(resume_ctx) = &config.resume_context {
generate_resume_note(resume_ctx)
} else if config.is_resume {
"\nNOTE: This session is resuming from a previous run. Previous progress is preserved in git history.\n\n".to_string()
} else {
String::new()
};
let base_prompt = match (role, action) {
(_, Action::Plan) => prompt_plan_with_context(
template_context,
config.prompt_md_content.as_deref(),
workspace,
),
(Role::Developer | Role::Reviewer, Action::Iterate) => {
let (prompt_content, plan_content) = config
.prompt_and_plan
.unwrap_or((String::new(), String::new()));
prompt_developer_iteration_with_context(
template_context,
config.iteration.unwrap_or(1),
config.total_iterations.unwrap_or(1),
context,
&prompt_content,
&plan_content,
)
}
(_, Action::Fix) => {
let (prompt_content, plan_content, issues_content) = config
.prompt_plan_and_issues
.unwrap_or((String::new(), String::new(), String::new()));
prompt_fix_with_context(
template_context,
&prompt_content,
&plan_content,
&issues_content,
workspace,
)
}
};
if config.is_resume {
format!("{resume_note}{base_prompt}")
} else {
base_prompt
}
}
pub fn get_stored_or_generate_prompt<F, S: std::hash::BuildHasher>(
scope_key: &PromptScopeKey,
prompt_history: &std::collections::HashMap<String, PromptHistoryEntry, S>,
current_content_id: Option<&str>,
generator: F,
) -> (String, bool)
where
F: FnOnce() -> String,
{
let key = scope_key.to_string();
if let Some(entry) = prompt_history.get(&key) {
let content_id_mismatch = current_content_id
.is_some_and(|current_id| entry.content_id.as_deref() != Some(current_id));
if content_id_mismatch {
(generator(), false)
} else {
(entry.content.clone(), true)
}
} else {
(generator(), false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prompts::prompt_scope_key::RetryMode;
#[test]
fn test_get_stored_or_generate_prompt_replays_when_available() {
let scope_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let history = [(
scope_key.to_string(),
PromptHistoryEntry::from_string("stored prompt".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, None, || {
"generated prompt".to_string()
});
assert_eq!(prompt, "stored prompt");
assert!(was_replayed, "Should have replayed the stored prompt");
}
#[test]
fn test_get_stored_or_generate_prompt_generates_when_not_available() {
let scope_key = PromptScopeKey::for_development(2, None, RetryMode::Normal, 0);
let history = std::collections::HashMap::new();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, None, || {
"generated prompt".to_string()
});
assert_eq!(prompt, "generated prompt");
assert!(!was_replayed, "Should have generated a new prompt");
}
#[test]
fn test_get_stored_or_generate_prompt_with_empty_history() {
let scope_key = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0);
let history = std::collections::HashMap::new();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, None, || {
"fresh prompt".to_string()
});
assert_eq!(prompt, "fresh prompt");
assert!(
!was_replayed,
"Should have generated a new prompt for empty history"
);
}
#[test]
fn test_key_lookup_uses_display_string() {
let scope_key = PromptScopeKey::for_commit(2, 1, RetryMode::Xsd { count: 1 }, 0);
let expected_key = "commit_message_attempt_iter2_1_xsd_retry_1";
let history = [(
expected_key.to_string(),
PromptHistoryEntry::from_string("stored xsd retry prompt".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, None, || "new prompt".to_string());
assert_eq!(prompt, "stored xsd retry prompt");
assert!(
was_replayed,
"Should replay using Display string '{expected_key}' as key"
);
}
#[test]
fn test_recovery_epoch_does_not_affect_lookup_key() {
let scope_key_epoch0 = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let scope_key_epoch1 = PromptScopeKey::for_planning(1, RetryMode::Normal, 1);
let history = [(
scope_key_epoch0.to_string(),
PromptHistoryEntry::from_string("stored".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key_epoch1, &history, None, || "new".to_string());
assert_eq!(prompt, "stored");
assert!(
was_replayed,
"Epoch change alone must not bust the history lookup key"
);
}
#[test]
fn test_content_id_match_replays_prompt() {
let scope_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let history = [(
scope_key.to_string(),
PromptHistoryEntry {
content: "stored prompt".to_string(),
content_id: Some("abc123".to_string()),
},
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, Some("abc123"), || {
"generated".to_string()
});
assert_eq!(prompt, "stored prompt");
assert!(was_replayed, "Should replay when content-ids match");
}
#[test]
fn test_content_id_mismatch_generates_fresh_prompt() {
let scope_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let history = [(
scope_key.to_string(),
PromptHistoryEntry {
content: "stale prompt".to_string(),
content_id: Some("old_hash".to_string()),
},
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, Some("new_hash"), || {
"fresh prompt".to_string()
});
assert_eq!(prompt, "fresh prompt");
assert!(
!was_replayed,
"Should generate fresh prompt when content-ids differ"
);
}
#[test]
fn test_legacy_entry_without_content_id_is_treated_as_miss_when_current_content_id_is_known() {
let scope_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let history = [(
scope_key.to_string(),
PromptHistoryEntry::from_string("legacy prompt".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, Some("any_hash"), || {
"generated".to_string()
});
assert_eq!(prompt, "generated");
assert!(
!was_replayed,
"Legacy entries with no stored content_id must not replay when current_content_id is Some"
);
}
#[test]
fn test_no_current_content_id_replays_without_validation() {
let scope_key = PromptScopeKey::for_planning(1, RetryMode::Normal, 0);
let history = [(
scope_key.to_string(),
PromptHistoryEntry {
content: "stored prompt".to_string(),
content_id: Some("some_hash".to_string()),
},
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&scope_key, &history, None, || "generated".to_string());
assert_eq!(prompt, "stored prompt");
assert!(
was_replayed,
"Should replay when current_content_id is None (caller does not validate)"
);
}
#[test]
fn iteration_2_development_does_not_replay_iteration_1_prompt() {
let iter1_key = PromptScopeKey::for_development(1, None, RetryMode::Normal, 0);
let history = [(
iter1_key.to_string(),
PromptHistoryEntry::from_string("iter 1 development prompt".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let iter2_key = PromptScopeKey::for_development(2, None, RetryMode::Normal, 0);
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&iter2_key, &history, None, || {
"iter 2 fresh development prompt".to_string()
});
assert!(
!was_replayed,
"iter2 development must NOT replay iter1 development prompt"
);
assert_eq!(
prompt, "iter 2 fresh development prompt",
"iter2 development must receive a freshly generated prompt"
);
}
#[test]
fn test_iteration_2_commit_does_not_replay_iteration_1_prompt() {
let iter1_key = PromptScopeKey::for_commit(1, 1, RetryMode::Normal, 0);
let history = [(
iter1_key.to_string(),
PromptHistoryEntry::from_string("iter 1 commit prompt".to_string()),
)]
.into_iter()
.collect::<std::collections::HashMap<_, _>>();
let iter2_key = PromptScopeKey::for_commit(2, 1, RetryMode::Normal, 0);
let (prompt, was_replayed) =
get_stored_or_generate_prompt(&iter2_key, &history, None, || {
"iter 2 fresh commit prompt".to_string()
});
assert!(
!was_replayed,
"iter2/attempt1 must NOT replay iter1/attempt1"
);
assert_eq!(
prompt, "iter 2 fresh commit prompt",
"iter2 must receive a freshly generated prompt, not iter1's stale content"
);
}
}