Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/src/ai/agent_management/agent_management_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,35 @@ impl AgentNotificationsModel {
ctx,
);
}
CLIAgentSessionStatus::Failed {
error_type,
message,
} => {
let title = session_context
.display_title()
.unwrap_or_else(|| format!("{} failed", agent.display_name()));
let body = match (message.as_deref(), error_type.as_deref()) {
(Some(msg), Some(kind)) => format!("{kind}: {msg}"),
(Some(msg), None) => msg.to_owned(),
(None, Some(kind)) => kind.to_owned(),
(None, None) => "The agent encountered an error.".to_owned(),
};
let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx);
self.add_notification(
title,
body,
NotificationCategory::Error,
NotificationSourceAgent::CLI {
agent: *agent,
is_ambient: metadata.is_ambient,
},
NotificationOrigin::CLISession(*terminal_view_id),
*terminal_view_id,
vec![],
metadata.branch,
ctx,
);
}
CLIAgentSessionStatus::Blocked { message } => {
let title = session_context
.display_title()
Expand Down
7 changes: 5 additions & 2 deletions app/src/ai/agent_sdk/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2853,7 +2853,8 @@ impl AgentDriver {
) {
log::info!(
"Ignoring runtime failure for {harness_name}: \
session already marked Success (pattern={}, excerpt={})",
session already marked Success or Failed via plugin \
(pattern={}, excerpt={})",
error.pattern,
error.excerpt,
);
Expand Down Expand Up @@ -3435,7 +3436,9 @@ impl AgentDriver {

// Drive idle-on-complete timer for the harness exit signal.
match status {
CLIAgentSessionStatus::Success | CLIAgentSessionStatus::Blocked { .. } => {
CLIAgentSessionStatus::Success
| CLIAgentSessionStatus::Failed { .. }
| CLIAgentSessionStatus::Blocked { .. } => {
if let Some(idle_timeout) = me.idle_on_complete {
log::info!(
"Ambient agent CLI lifecycle: event=idle_timeout_scheduled task_id={:?} terminal_view_id={terminal_view_id:?} timeout={idle_timeout:?}",
Expand Down
1 change: 1 addition & 0 deletions app/src/ai/agent_sdk/driver/harness/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ impl ClaudeHarnessRunner {
cli_agent_session_status(&self.terminal_driver, foreground).await,
Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::Blocked { .. })
| Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::InProgress)
| Some(crate::terminal::cli_agent_sessions::CLIAgentSessionStatus::Failed { .. })
)
}

Expand Down
3 changes: 3 additions & 0 deletions app/src/ai/agent_sdk/driver/harness/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ impl ThirdPartyHarness for CodexHarness {
// OAuth refresh failures — all five Codex variants share this
// substring (see upstream session/token messages).
"could not be refreshed",
// Generically check for invalid request errors.
// Keep this last so more specific patterns can be matched first.
"\"type\": \"invalid_request_error\"",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex doesn't have an equivalent hook. We add this runtime pattern match to fix this run not properly being marked as errored https://oz.staging.warp.dev/runs/019f6324-bb99-70fa-aa53-d200dd806dca

]
}

Expand Down
7 changes: 6 additions & 1 deletion app/src/ai/agent_sdk/driver/harness_output_monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,12 @@ pub(crate) async fn watch_block_for_errors(
}

pub(crate) fn should_suppress_runtime_failure(status: Option<&CLIAgentSessionStatus>) -> bool {
matches!(status, Some(CLIAgentSessionStatus::Success))
// On CLIAgentSessionStatus::Failed, we directly update the task status,
// so we don't need to do runtime pattern matching to find the failure.
matches!(
status,
Some(CLIAgentSessionStatus::Success) | Some(CLIAgentSessionStatus::Failed { .. })
)
}

/// Cap excerpt length so we don't blow up status messages or logs with
Expand Down
16 changes: 16 additions & 0 deletions app/src/ai/blocklist/local_agent_task_sync_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,22 @@ fn map_cli_session_status(
match status {
CLIAgentSessionStatus::InProgress => (AgentTaskState::InProgress, None),
CLIAgentSessionStatus::Success => (AgentTaskState::Succeeded, None),
CLIAgentSessionStatus::Failed {
error_type,
message,
} => {
// User-actionable errors (bad credentials, org restrictions, billing) map to
// FAILED. Everything else (rate limits, server errors, model errors, etc.)
// maps to ERROR since they are typically Anthropic's or Warp's fault.
// The list of error types on Claude Code comes from https://code.claude.com/docs/en/hooks#stopfailure-input
let task_state = match error_type.as_deref() {
Some("authentication_failed" | "oauth_org_not_allowed" | "billing_error") => {
AgentTaskState::Failed
}
_ => AgentTaskState::Error,
};
Comment on lines +559 to +564

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Classify user-actionable request failures as Failed; with the current _ arm, the invalid-model case from the PR description reports as a provider/Warp Error instead of a user-fixable failure.

Suggested change
let task_state = match error_type.as_deref() {
Some("authentication_failed" | "oauth_org_not_allowed" | "billing_error") => {
AgentTaskState::Failed
}
_ => AgentTaskState::Error,
};
let task_state = match error_type.as_deref() {
Some(
"authentication_failed"
| "oauth_org_not_allowed"
| "billing_error"
| "invalid_request"
| "model_not_found"
| "max_output_tokens",
) => AgentTaskState::Failed,
_ => AgentTaskState::Error,
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is intentional. The other error categories mean warp configured something incorrectly

Comment on lines +559 to +564

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] model_not_found is a user-actionable bad model selection (the PR’s testing reproduces this with an invalid ANTHROPIC_MODEL), so mapping it through _ => Error misclassifies the task as a system/provider error.

Suggested change
let task_state = match error_type.as_deref() {
Some("authentication_failed" | "oauth_org_not_allowed" | "billing_error") => {
AgentTaskState::Failed
}
_ => AgentTaskState::Error,
};
let task_state = match error_type.as_deref() {
Some(
"authentication_failed"
| "oauth_org_not_allowed"
| "billing_error"
| "model_not_found",
) => AgentTaskState::Failed,
_ => AgentTaskState::Error,
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect. Warp is responsible for setting the model and validating it before starting the worker

#13784 (comment)

(task_state, message.as_ref().map(TaskStatusUpdate::message))
}
CLIAgentSessionStatus::Blocked { message } => (
AgentTaskState::Blocked,
message.as_ref().map(TaskStatusUpdate::message),
Expand Down
4 changes: 4 additions & 0 deletions app/src/terminal/cli_agent_sessions/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum CLIAgentEventType {
PromptSubmit,
ToolComplete,
Stop,
StopFailure,
PermissionRequest,
PermissionReplied,
QuestionAsked,
Expand Down Expand Up @@ -46,6 +47,9 @@ pub struct CLIAgentEventPayload {
pub tool_name: Option<String>,
pub tool_input_preview: Option<String>,
pub plugin_version: Option<String>,
/// On Claude Code, this comes from the `StopFailure` hook (e.g. `"rate_limit"`).
/// Not implemented for Codex.
pub error_type: Option<String>,
}

/// A parsed event from a CLI agent plugin.
Expand Down
3 changes: 3 additions & 0 deletions app/src/terminal/cli_agent_sessions/event/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(super) fn parse(body: &str) -> Option<CLIAgentEvent> {
"prompt_submit" => CLIAgentEventType::PromptSubmit,
"tool_complete" => CLIAgentEventType::ToolComplete,
"stop" => CLIAgentEventType::Stop,
"stop_failure" => CLIAgentEventType::StopFailure,
"permission_request" => CLIAgentEventType::PermissionRequest,
"permission_replied" => CLIAgentEventType::PermissionReplied,
"question_asked" => CLIAgentEventType::QuestionAsked,
Expand Down Expand Up @@ -53,6 +54,7 @@ pub(super) fn parse(body: &str) -> Option<CLIAgentEvent> {
tool_name: raw.tool_name,
tool_input_preview,
plugin_version: raw.plugin_version,
error_type: raw.error_type,
},
source: CLIAgentEventSource::RichPlugin,
})
Expand All @@ -73,4 +75,5 @@ struct RawEvent {
tool_name: Option<String>,
tool_input: Option<serde_json::Value>,
plugin_version: Option<String>,
error_type: Option<String>,
}
18 changes: 17 additions & 1 deletion app/src/terminal/cli_agent_sessions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ use crate::ai::blocklist::InputConfig;
pub enum CLIAgentSessionStatus {
InProgress,
Success,
Blocked { message: Option<String> },
Failed {
error_type: Option<String>,
message: Option<String>,
},
Blocked {
message: Option<String>,
},
}

impl CLIAgentSessionStatus {
Expand All @@ -26,6 +32,7 @@ impl CLIAgentSessionStatus {
match self {
CLIAgentSessionStatus::InProgress => ConversationStatus::InProgress,
CLIAgentSessionStatus::Success => ConversationStatus::Success,
CLIAgentSessionStatus::Failed { .. } => ConversationStatus::Error,
CLIAgentSessionStatus::Blocked { message } => ConversationStatus::Blocked {
blocked_action: message.clone().unwrap_or_default(),
},
Expand Down Expand Up @@ -206,6 +213,15 @@ impl CLIAgentSession {
self.clear_permission_scoped_state();
CLIAgentSessionStatus::Success
}
CLIAgentEventType::StopFailure => {
self.session_context.query = event.payload.query.clone();
self.session_context.response = event.payload.response.clone();
self.clear_permission_scoped_state();
CLIAgentSessionStatus::Failed {
error_type: event.payload.error_type.clone(),
message: event.payload.response.clone(),
}
}
Comment thread
seemeroland marked this conversation as resolved.
CLIAgentEventType::PermissionRequest => {
self.session_context.summary = event.payload.summary.clone();
self.session_context.tool_name = event.payload.tool_name.clone();
Expand Down
6 changes: 5 additions & 1 deletion app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13479,7 +13479,9 @@ impl TerminalView {
ctx,
);
}
CLIAgentSessionStatus::InProgress | CLIAgentSessionStatus::Success => {
CLIAgentSessionStatus::InProgress
| CLIAgentSessionStatus::Success
| CLIAgentSessionStatus::Failed { .. } => {
// Auto-open rich input when the agent resumes or completes.
if !self.has_active_cli_agent_input_session(ctx) {
self.open_cli_agent_rich_input(CLIAgentInputEntrypoint::AutoShow, ctx);
Expand Down Expand Up @@ -13511,6 +13513,8 @@ impl TerminalView {

let trigger = if matches!(status, CLIAgentSessionStatus::Blocked { .. }) {
NotificationsTrigger::NeedsAttention
} else if matches!(status, CLIAgentSessionStatus::Failed { .. }) {
NotificationsTrigger::AgentTaskCompleted(false)
} else {
NotificationsTrigger::AgentTaskCompleted(true)
};
Expand Down
Loading