use vtcode_core::exec::events::{
ContextResetEvent, ContextResetTrigger, HarnessEventKind, ItemCompletedEvent, ItemStartedEvent,
PlanApprovalDecision, PlanApprovalRequestedEvent, PlanApprovalResolvedEvent, PlanDeltaEvent, PlanItem, ThreadEvent,
ThreadItem, ThreadItemDetails,
};
use super::PlanningWorkflowState;
use crate::agent::runloop::unified::inline_events::harness::HarnessEventEmitter;
use crate::agent::runloop::unified::inline_events::harness::harness_event;
use crate::agent::runloop::unified::planning_workflow_state::PlanningWorkflowSessionState;
pub(crate) async fn emit_plan_ready_events(
plan_session: &mut PlanningWorkflowSessionState,
plan_state: &PlanningWorkflowState,
emitter: Option<&HarnessEventEmitter>,
thread_id: &str,
turn_id: &str,
plan_text: &str,
) {
plan_session.mark_plan_approval_pending(thread_id.to_owned(), turn_id.to_owned());
let Some(emitter) = emitter else {
return;
};
let item_id = format!("{turn_id}-plan");
let plan_path = plan_state.get_plan_file().await.map(|path| path.display().to_string());
let _ = emitter.emit(harness_event(
HarnessEventKind::PlanningStarted,
Some("Planning workflow produced a plan for review.".to_string()),
plan_path.clone(),
None,
None,
));
let start_item = ThreadItem {
id: item_id.clone(),
details: ThreadItemDetails::Plan(PlanItem { text: String::new() }),
};
let _ = emitter.emit(ThreadEvent::ItemStarted(ItemStartedEvent { item: start_item }));
let _ = emitter.emit(ThreadEvent::PlanDelta(PlanDeltaEvent {
thread_id: thread_id.to_owned(),
turn_id: turn_id.to_owned(),
item_id: item_id.clone(),
delta: plan_text.to_owned(),
}));
let completed_item = ThreadItem {
id: item_id,
details: ThreadItemDetails::Plan(PlanItem { text: plan_text.to_owned() }),
};
let _ = emitter.emit(ThreadEvent::ItemCompleted(ItemCompletedEvent { item: completed_item }));
let _ = emitter.emit(harness_event(
HarnessEventKind::PlanningCompleted,
Some("Plan is ready for user approval.".to_string()),
plan_path.clone(),
None,
None,
));
emit_plan_approval_requested(Some(emitter), thread_id, turn_id, plan_path);
}
pub(crate) fn emit_plan_approval_requested(
emitter: Option<&HarnessEventEmitter>,
thread_id: impl Into<String>,
turn_id: impl Into<String>,
plan_file: Option<String>,
) {
let Some(emitter) = emitter else {
return;
};
if let Err(err) = emitter.emit(ThreadEvent::PlanApprovalRequested(PlanApprovalRequestedEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
plan_file,
})) {
tracing::debug!(error = %err, "failed to emit plan approval request event");
}
}
pub(crate) fn emit_context_reset(
emitter: Option<&HarnessEventEmitter>,
thread_id: impl Into<String>,
turn_id: impl Into<String>,
previous_context_usage_percent: u8,
) {
let Some(emitter) = emitter else {
return;
};
if let Err(err) = emitter.emit(ThreadEvent::ContextReset(ContextResetEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
trigger: ContextResetTrigger::PlanApproval,
plan_preserved: true,
previous_context_usage_percent,
tool_budget_reset: true,
})) {
tracing::debug!(error = %err, "failed to emit context reset event");
}
}
pub(crate) fn emit_plan_approval_resolved(
emitter: Option<&HarnessEventEmitter>,
thread_id: impl Into<String>,
turn_id: impl Into<String>,
decision: PlanApprovalDecision,
automatic: bool,
) {
let Some(emitter) = emitter else {
return;
};
if let Err(err) = emitter.emit(ThreadEvent::PlanApprovalResolved(PlanApprovalResolvedEvent {
thread_id: thread_id.into(),
turn_id: turn_id.into(),
decision,
automatic,
})) {
tracing::debug!(error = %err, "failed to emit plan approval resolution event");
}
}
#[cfg(test)]
mod tests {
use super::{emit_plan_approval_requested, emit_plan_approval_resolved};
use std::path::PathBuf;
use tempfile::tempdir;
use vtcode_core::exec::events::{PlanApprovalDecision, ThreadEvent, VersionedThreadEvent};
#[test]
fn approval_events_are_written_to_the_shared_harness_stream() {
let directory = tempdir().expect("temporary event directory");
let path = directory.path().join(PathBuf::from("events.jsonl"));
let emitter = super::HarnessEventEmitter::new(path.clone()).expect("harness emitter");
emit_plan_approval_requested(Some(&emitter), "thread-1", "turn-1", Some(".vtcode/plans/task.md".to_string()));
emit_plan_approval_resolved(Some(&emitter), "thread-1", "turn-2", PlanApprovalDecision::SwitchAuto, false);
let lines = std::fs::read_to_string(path)
.expect("event log")
.lines()
.map(|line| serde_json::from_str::<VersionedThreadEvent>(line).expect("versioned event"))
.map(VersionedThreadEvent::into_event)
.collect::<Vec<_>>();
assert!(matches!(lines[0], ThreadEvent::PlanApprovalRequested(_)));
assert!(matches!(
lines[1],
ThreadEvent::PlanApprovalResolved(ref event)
if event.decision == PlanApprovalDecision::SwitchAuto && !event.automatic
));
}
}