use crate::reducer::event::PromptInputEvent;
use crate::reducer::state::{
MaterializedCommitInputs, MaterializedDevelopmentInputs, MaterializedPlanningInputs,
MaterializedReviewInputs, MaterializedXsdRetryLastOutput, PipelineState,
};
pub fn reduce_prompt_input_event(state: PipelineState, event: PromptInputEvent) -> PipelineState {
match event {
PromptInputEvent::OversizeDetected { .. } => state,
PromptInputEvent::PlanningInputsMaterialized { iteration, prompt } => PipelineState {
prompt_inputs: crate::reducer::state::PromptInputsState {
planning: Some(MaterializedPlanningInputs { iteration, prompt }),
..state.prompt_inputs
},
..state
},
PromptInputEvent::DevelopmentInputsMaterialized {
iteration,
prompt,
plan,
} => PipelineState {
prompt_inputs: crate::reducer::state::PromptInputsState {
development: Some(MaterializedDevelopmentInputs {
iteration,
prompt,
plan,
}),
..state.prompt_inputs
},
..state
},
PromptInputEvent::ReviewInputsMaterialized { pass, plan, diff } => PipelineState {
prompt_inputs: crate::reducer::state::PromptInputsState {
review: Some(MaterializedReviewInputs { pass, plan, diff }),
..state.prompt_inputs
},
..state
},
PromptInputEvent::CommitInputsMaterialized { attempt, diff } => PipelineState {
prompt_inputs: crate::reducer::state::PromptInputsState {
commit: Some(MaterializedCommitInputs { attempt, diff }),
..state.prompt_inputs
},
..state
},
PromptInputEvent::XsdRetryLastOutputMaterialized {
phase,
scope_id,
last_output,
} => PipelineState {
prompt_inputs: crate::reducer::state::PromptInputsState {
xsd_retry_last_output: Some(MaterializedXsdRetryLastOutput {
phase,
scope_id,
last_output,
}),
..state.prompt_inputs
},
..state
},
PromptInputEvent::HandlerError { error, .. } => super::error::reduce_error(&state, &error),
PromptInputEvent::PromptPermissionsLocked { warning } => {
let locked = warning.is_none();
PipelineState {
prompt_permissions: crate::reducer::state::PromptPermissionsState {
locked,
restore_needed: locked,
restored: false,
last_warning: warning,
},
..state
}
}
PromptInputEvent::PromptPermissionsRestoreWarning { warning } => PipelineState {
prompt_permissions: crate::reducer::state::PromptPermissionsState {
last_warning: Some(warning),
..state.prompt_permissions
},
..state
},
PromptInputEvent::TemplateRendered {
phase: _,
template_name: _,
log,
} => {
let validation_failed = !log.is_complete();
let unsubstituted = if validation_failed {
log.unsubstituted.clone()
} else {
Vec::new()
};
PipelineState {
last_substitution_log: Some(log),
template_validation_failed: validation_failed,
template_validation_unsubstituted: unsubstituted,
..state
}
}
PromptInputEvent::PromptCaptured {
key,
content,
content_id,
} => {
let entry = crate::prompts::PromptHistoryEntry {
content,
content_id,
};
let final_entry = match state.prompt_history.get(&key) {
None => entry,
Some(existing) => {
let is_same_content = existing.content == entry.content;
let would_downgrade_id =
existing.content_id.is_some() && entry.content_id.is_none();
let is_exact_same = existing.content == entry.content
&& existing.content_id == entry.content_id;
if is_same_content && would_downgrade_id {
existing.clone()
} else if is_exact_same {
existing.clone()
} else {
entry
}
}
};
let new_history = state
.prompt_history
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.chain(std::iter::once((key, final_entry)))
.collect();
PipelineState {
prompt_history: new_history,
..state
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reducer::event::PipelineEvent;
use crate::reducer::state::PipelineState;
pub(crate) fn reduce(state: PipelineState, event: PipelineEvent) -> PipelineState {
match event {
PipelineEvent::PromptInput(e) => reduce_prompt_input_event(state, e),
_ => panic!("unexpected event in test"),
}
}
#[test]
fn test_prompt_captured_adds_to_state_prompt_history() {
let state = PipelineState::initial(1, 0);
assert!(
state.prompt_history.is_empty(),
"initial state has no prompt history"
);
let new_state = reduce(
state,
PipelineEvent::PromptInput(PromptInputEvent::PromptCaptured {
key: "planning_1".to_string(),
content: "test planning prompt".to_string(),
content_id: Some("sha256-abc".to_string()),
}),
);
let entry = new_state
.prompt_history
.get("planning_1")
.expect("entry must be present after PromptCaptured");
assert_eq!(entry.content, "test planning prompt");
assert_eq!(entry.content_id, Some("sha256-abc".to_string()));
}
#[test]
fn test_prompt_captured_overwrites_existing_when_content_differs_even_if_content_id_same() {
let state = PipelineState::initial(1, 0);
let history = state
.prompt_history
.into_iter()
.chain(std::iter::once((
"planning_1".to_string(),
crate::prompts::PromptHistoryEntry {
content: "original prompt".to_string(),
content_id: Some("sha256-same".to_string()),
},
)))
.collect::<std::collections::HashMap<_, _>>();
let state = PipelineState {
prompt_history: history,
..state
};
let new_state = reduce(
state,
PipelineEvent::PromptInput(PromptInputEvent::PromptCaptured {
key: "planning_1".to_string(),
content: "replacement prompt".to_string(),
content_id: Some("sha256-same".to_string()),
}),
);
let entry = new_state
.prompt_history
.get("planning_1")
.expect("entry must still be present");
assert_eq!(entry.content, "replacement prompt");
assert_eq!(entry.content_id, Some("sha256-same".to_string()));
}
#[test]
fn test_prompt_captured_does_not_downgrade_existing_content_id_when_content_is_identical() {
let state = PipelineState::initial(1, 0);
let history = state
.prompt_history
.into_iter()
.chain(std::iter::once((
"planning_1".to_string(),
crate::prompts::PromptHistoryEntry {
content: "same prompt".to_string(),
content_id: Some("sha256-keep".to_string()),
},
)))
.collect::<std::collections::HashMap<_, _>>();
let state = PipelineState {
prompt_history: history,
..state
};
let new_state = reduce(
state,
PipelineEvent::PromptInput(PromptInputEvent::PromptCaptured {
key: "planning_1".to_string(),
content: "same prompt".to_string(),
content_id: None,
}),
);
let entry = new_state
.prompt_history
.get("planning_1")
.expect("entry must be present");
assert_eq!(entry.content, "same prompt");
assert_eq!(entry.content_id.as_deref(), Some("sha256-keep"));
}
#[test]
fn test_prompt_captured_overwrites_existing_when_content_id_differs() {
let state = PipelineState::initial(1, 0);
let history = state
.prompt_history
.into_iter()
.chain(std::iter::once((
"planning_1".to_string(),
crate::prompts::PromptHistoryEntry {
content: "stale prompt".to_string(),
content_id: Some("sha256-old".to_string()),
},
)))
.collect::<std::collections::HashMap<_, _>>();
let state = PipelineState {
prompt_history: history,
..state
};
let new_state = reduce(
state,
PipelineEvent::PromptInput(PromptInputEvent::PromptCaptured {
key: "planning_1".to_string(),
content: "fresh prompt".to_string(),
content_id: Some("sha256-new".to_string()),
}),
);
let entry = new_state
.prompt_history
.get("planning_1")
.expect("entry must be present");
assert_eq!(entry.content, "fresh prompt");
assert_eq!(entry.content_id.as_deref(), Some("sha256-new"));
}
#[test]
fn test_prompt_captured_overwrites_existing_when_incoming_has_no_content_id_and_content_differs(
) {
let state = PipelineState::initial(1, 0);
let history = state
.prompt_history
.into_iter()
.chain(std::iter::once((
"planning_1".to_string(),
crate::prompts::PromptHistoryEntry {
content: "stale prompt".to_string(),
content_id: None,
},
)))
.collect::<std::collections::HashMap<_, _>>();
let state = PipelineState {
prompt_history: history,
..state
};
let new_state = reduce(
state,
PipelineEvent::PromptInput(PromptInputEvent::PromptCaptured {
key: "planning_1".to_string(),
content: "fresh prompt".to_string(),
content_id: None,
}),
);
let entry = new_state
.prompt_history
.get("planning_1")
.expect("entry must be present");
assert_eq!(entry.content, "fresh prompt");
assert!(entry.content_id.is_none());
}
}