feat: make max iterations resumable with Continue button
- Mark missions as "blocked" instead of "failed" when max iterations reached - Allow resuming blocked missions (same as interrupted) - Add "Continue Mission" button in UI for blocked missions - Show iteration limit banner in conversation view
This commit is contained in:
@@ -753,13 +753,13 @@ export default function ControlClient() {
|
||||
|
||||
// Handle resuming an interrupted mission
|
||||
const handleResumeMission = async () => {
|
||||
if (!currentMission || currentMission.status !== "interrupted") return;
|
||||
if (!currentMission || !["interrupted", "blocked"].includes(currentMission.status)) return;
|
||||
try {
|
||||
setMissionLoading(true);
|
||||
const resumed = await resumeMission(currentMission.id);
|
||||
setCurrentMission(resumed);
|
||||
setShowStatusMenu(false);
|
||||
toast.success("Mission resumed");
|
||||
toast.success(currentMission.status === "blocked" ? "Continuing mission" : "Mission resumed");
|
||||
} catch (err) {
|
||||
console.error("Failed to resume mission:", err);
|
||||
toast.error("Failed to resume mission");
|
||||
@@ -1439,16 +1439,36 @@ export default function ControlClient() {
|
||||
) : currentMission && currentMission.status !== "active" ? (
|
||||
<>
|
||||
<h2 className="text-lg font-medium text-white">
|
||||
{currentMission.status === "interrupted" ? "Mission Interrupted" : "No conversation history"}
|
||||
{currentMission.status === "interrupted"
|
||||
? "Mission Interrupted"
|
||||
: currentMission.status === "blocked"
|
||||
? "Iteration Limit Reached"
|
||||
: "No conversation history"}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-white/40 max-w-sm">
|
||||
{currentMission.status === "interrupted" ? (
|
||||
<>This mission was interrupted (server shutdown or cancellation). Click the <strong className="text-amber-400">Resume</strong> button in the mission menu to continue where you left off.</>
|
||||
) : currentMission.status === "blocked" ? (
|
||||
<>The agent reached its iteration limit (50). You can continue the mission to give it more iterations.</>
|
||||
) : (
|
||||
<>This mission was {currentMission.status} without any messages.
|
||||
{currentMission.status === "completed" && " You can reactivate it to continue."}</>
|
||||
)}
|
||||
</p>
|
||||
{currentMission.status === "blocked" && (
|
||||
<button
|
||||
onClick={handleResumeMission}
|
||||
disabled={missionLoading}
|
||||
className="mt-4 inline-flex items-center gap-2 rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{missionLoading ? (
|
||||
<Loader className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<PlayCircle className="h-4 w-4" />
|
||||
)}
|
||||
Continue Mission
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -1732,6 +1752,32 @@ export default function ControlClient() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Continue banner for blocked missions */}
|
||||
{currentMission?.status === "blocked" && items.length > 0 && (
|
||||
<div className="flex justify-center py-4">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-amber-500/10 border border-amber-500/20 px-5 py-3">
|
||||
<Clock className="h-5 w-5 text-amber-400" />
|
||||
<div className="text-sm">
|
||||
<span className="text-amber-400 font-medium">Iteration limit reached</span>
|
||||
<span className="text-white/50 ml-1">— Agent used all 50 iterations</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleResumeMission}
|
||||
disabled={missionLoading}
|
||||
className="ml-2 inline-flex items-center gap-1.5 rounded-lg bg-amber-500 px-3 py-1.5 text-sm font-medium text-black hover:bg-amber-400 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{missionLoading ? (
|
||||
<Loader className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<PlayCircle className="h-3.5 w-3.5" />
|
||||
)}
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1328,7 +1328,7 @@ async fn control_actor_loop(
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to build resume context for an interrupted mission
|
||||
// Helper to build resume context for an interrupted or blocked mission
|
||||
async fn resume_mission_impl(
|
||||
memory: &Option<MemorySystem>,
|
||||
config: &Config,
|
||||
@@ -1336,8 +1336,8 @@ async fn control_actor_loop(
|
||||
) -> Result<(Mission, String), String> {
|
||||
let mission = load_mission_from_db(memory, mission_id).await?;
|
||||
|
||||
// Check if mission can be resumed
|
||||
if mission.status != MissionStatus::Interrupted {
|
||||
// Check if mission can be resumed (interrupted or blocked)
|
||||
if !matches!(mission.status, MissionStatus::Interrupted | MissionStatus::Blocked) {
|
||||
return Err(format!(
|
||||
"Mission {} cannot be resumed (status: {})",
|
||||
mission_id, mission.status
|
||||
@@ -1347,14 +1347,19 @@ async fn control_actor_loop(
|
||||
// Build resume context
|
||||
let mut resume_parts = Vec::new();
|
||||
|
||||
// Add interruption notice
|
||||
// Add resumption notice based on status
|
||||
let resume_reason = match mission.status {
|
||||
MissionStatus::Blocked => "reached its iteration limit",
|
||||
_ => "was interrupted",
|
||||
};
|
||||
|
||||
if let Some(interrupted_at) = &mission.interrupted_at {
|
||||
resume_parts.push(format!(
|
||||
"**MISSION RESUMED**\nThis mission was interrupted at {} and is now being continued.",
|
||||
interrupted_at
|
||||
"**MISSION RESUMED**\nThis mission {} at {} and is now being continued.",
|
||||
resume_reason, interrupted_at
|
||||
));
|
||||
} else {
|
||||
resume_parts.push("**MISSION RESUMED**\nThis mission was interrupted and is now being continued.".to_string());
|
||||
resume_parts.push(format!("**MISSION RESUMED**\nThis mission {} and is now being continued.", resume_reason));
|
||||
}
|
||||
|
||||
// Add history summary
|
||||
@@ -1966,7 +1971,20 @@ async fn control_actor_loop(
|
||||
.map(|m| m.status);
|
||||
|
||||
if current_status.as_deref() == Some("active") {
|
||||
let status = if agent_result.success { "completed" } else { "failed" };
|
||||
// Determine status based on terminal reason
|
||||
// MaxIterations -> blocked (resumable) instead of failed
|
||||
let (status, new_status) = match agent_result.terminal_reason {
|
||||
Some(TerminalReason::MaxIterations) => {
|
||||
("blocked", MissionStatus::Blocked)
|
||||
}
|
||||
_ if agent_result.success => {
|
||||
("completed", MissionStatus::Completed)
|
||||
}
|
||||
_ => {
|
||||
("failed", MissionStatus::Failed)
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"Auto-completing mission {} with status '{}' (terminal_reason: {:?})",
|
||||
mission_id, status, agent_result.terminal_reason
|
||||
@@ -1975,11 +1993,6 @@ async fn control_actor_loop(
|
||||
tracing::warn!("Failed to auto-complete mission: {}", e);
|
||||
} else {
|
||||
// Emit status change event
|
||||
let new_status = if agent_result.success {
|
||||
MissionStatus::Completed
|
||||
} else {
|
||||
MissionStatus::Failed
|
||||
};
|
||||
let _ = events_tx.send(AgentEvent::MissionStatusChanged {
|
||||
mission_id,
|
||||
status: new_status,
|
||||
|
||||
Reference in New Issue
Block a user